浏览代码

* Add FCM push notifications

Michaël Van Canneyt 1 年之前
父节点
当前提交
daf2dd869b

+ 61 - 0
packages/fcl-web/examples/fcm/README.md

@@ -0,0 +1,61 @@
+# Firebase Cloud Messaging demo
+
+This demo show how to use the fpfcmsender unit to send push notification messages to
+all kinds of devices using Google Firebase Cloud Messaging services.
+
+## Setup
+
+### You need a Firebase project. 
+
+### Web Client setup:
+
+   - For this project, under Messaging - WebPush, you need to create a VAPID key. 
+     The value of this key must be entered in the webclient, in the project file:
+     webclient/webclient.lpr, in the constant "TheVAPIDKey".
+
+   - The firebase application config must be saved in a config.js file, with
+     the following content (the keys must obviously be filled with the right
+     content): 
+```
+  var firebaseConfig = {
+    apiKey: "",
+    authDomain: "",
+    projectId: "",
+    storageBucket: "",
+    messagingSenderId: "",
+    appId: ""
+   } 
+
+### Server setup
+
+For the server project, you need to create a service account for your
+firebase application, and download the configuration file for this account. 
+This is a JSON file which contains the credentials for the service
+account.
+
+The JSON should be saved in a file called
+```
+messagingserver-serviceaccount.json
+```
+It will be loaded by the server when communicaton with FCM servers is
+needed.
+
+### HTTPS 
+
+Normally, the files and JSON-RPC calls should be using the HTTPS protocol.
+So either 
+
+- You configure the server to use SSL and provide a certificate.
+- You set up a webserver with HTTP snd forward the requests to the
+  application server
+- For testing purposes, you can configure the browser to accept HTTP for
+  localhost requests and allow a service worker on http.
+
+### Runnng the client
+The client application needs to be compiled with pas2js. 
+When executed in the browser, the 'Register' button must be used to register
+the application with Firebase. The browser will ask you if notifications must be
+allowed, and you must allow this or the application will not function.
+
+When done, you can enter a message and press the 'Send' button to send a
+notification message to yourself.

+ 76 - 0
packages/fcl-web/examples/fcm/server/messagingserver.lpi

@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="12"/>
+    <General>
+      <Flags>
+        <SaveOnlyProjectUnits Value="True"/>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+        <MainUnitHasScaledStatement Value="False"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <Title Value="messagingserver"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <BuildModes>
+      <Item Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+    </RunParams>
+    <Units>
+      <Unit>
+        <Filename Value="messagingserver.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="module.rpc.pp"/>
+        <IsPartOfProject Value="True"/>
+        <ComponentName Value="rpcModule"/>
+        <HasResources Value="True"/>
+        <ResourceBaseClass Value="DataModule"/>
+      </Unit>
+      <Unit>
+        <Filename Value="module.messaging.pp"/>
+        <IsPartOfProject Value="True"/>
+        <ComponentName Value="dmMessaging"/>
+        <HasResources Value="True"/>
+        <ResourceBaseClass Value="DataModule"/>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target>
+      <Filename Value="messagingserver"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+    </SearchPaths>
+    <Linking>
+      <Debugging>
+        <DebugInfoType Value="dsDwarf3"/>
+      </Debugging>
+    </Linking>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions>
+      <Item>
+        <Name Value="EAbort"/>
+      </Item>
+      <Item>
+        <Name Value="ECodetoolError"/>
+      </Item>
+      <Item>
+        <Name Value="EFOpenError"/>
+      </Item>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 36 - 0
packages/fcl-web/examples/fcm/server/messagingserver.lpr

@@ -0,0 +1,36 @@
+{
+    This file is part of the Free Component Library
+    Copyright (c) 2024 by Michael Van Canneyt [email protected]
+
+    FCM (Firebase Cloud Messaging) - Demo program
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+program messagingserver;
+
+{$mode objfpc}{$H+}
+
+uses
+  {$ifdef unix}
+  cwstring, cthreads,
+  {$ENDIF}
+  fpwebfile, fpwebclient, opensslsockets, fphttpwebclient,
+  fphttpapp, module.rpc, module.messaging;
+
+begin
+  DefaultWebClientClass:=TFPHTTPWebClient;
+  TSimpleFileModule.BaseDir:='./';
+  TSimpleFileModule.RegisterDefaultRoute;
+  Application.Title:='FCM Send message demo';
+  Application.Port:=8910;
+  Application.Threaded:=True;
+  Application.Initialize;
+  Application.Run;
+end.
+

+ 24 - 0
packages/fcl-web/examples/fcm/server/module.messaging.lfm

@@ -0,0 +1,24 @@
+object dmMessaging: TdmMessaging
+  OldCreateOrder = False
+  Height = 263
+  HorizontalOffset = 561
+  VerticalOffset = 273
+  Width = 439
+  object RegisterSubscription: TJSONRPCHandler
+    OnExecute = RegisterSubscriptionExecute
+    Options = []
+    ParamDefs = <    
+      item
+        Name = 'Token'
+      end>
+    Left = 98
+    Top = 128
+  end
+  object SendNotification: TJSONRPCHandler
+    OnExecute = SendNotificationExecute
+    Options = []
+    ParamDefs = <>
+    Left = 96
+    Top = 63
+  end
+end

+ 186 - 0
packages/fcl-web/examples/fcm/server/module.messaging.pp

@@ -0,0 +1,186 @@
+{
+    This file is part of the Free Component Library
+    Copyright (c) 2024 by Michael Van Canneyt [email protected]
+
+    FCM (Firebase Cloud Messaging) - JSON-RPC interface for webclient
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+unit module.messaging;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, fpjsonrpc, fpjson, fpfcmtypes, fpfcmsender;
+
+type
+
+  { TdmMessaging }
+
+  TdmMessaging = class(TDataModule)
+    RegisterSubscription: TJSONRPCHandler;
+    SendNotification: TJSONRPCHandler;
+    procedure RegisterSubscriptionExecute(Sender: TObject; const Params: TJSONData; out Res: TJSONData);
+    procedure SendNotificationExecute(Sender: TObject; const Params: TJSONData; out Res: TJSONData);
+  private
+    class function ConfigDir: String;
+    function AccessTokenFile: string;
+    function DeviceTokensFileName: String;
+    procedure HandleNewAccessToken(Sender: TObject; const aToken: TBearerToken);
+    function LoadLastToken: UTF8String;
+  public
+    procedure SendMessage(Msg: TNotificationmessage);
+    procedure SaveToken(const aToken: UTF8String);
+  end;
+
+var
+  dmMessaging: TdmMessaging;
+
+implementation
+
+{$R *.lfm}
+
+{ TdmMessaging }
+
+
+class Function TdmMessaging.ConfigDir : String;
+
+begin
+  Result:=ExtractFilePath(ParamStr(0));
+end;
+
+Function TdmMessaging.DeviceTokensFileName : String;
+begin
+  Result:=ConfigDir+'device-tokens.txt';
+end;
+
+Function TdmMessaging.AccessTokenFile : string;
+
+begin
+  Result:=ConfigDir+'access-token.json';
+end;
+
+procedure TdmMessaging.HandleNewAccessToken(Sender: TObject; const aToken: TBearerToken);
+
+begin
+  aToken.SaveToFile(AccessTokenFile);
+end;
+
+procedure TdmMessaging.SaveToken(const aToken : UTF8String);
+
+var
+  L : TStrings;
+  FN : String;
+
+begin
+  FN:=DeviceTokensFileName;
+  L:=TStringList.Create;
+  try
+    if FileExists(FN) then
+      L.LoadFromFile(FN);
+    L.Add(aToken);
+    L.SaveToFile(FN);
+  finally
+    L.Free;
+  end;
+end;
+
+function TdmMessaging.LoadLastToken : UTF8String;
+
+var
+  L : TStrings;
+  FN : String;
+
+begin
+  FN:=DeviceTokensFileName;
+  L:=TStringList.Create;
+  try
+    if fileExists(fn) then
+      L.LoadFromFile(FN);
+    if L.Count=0 then
+      Raise Exception.Create('No tokens registered');
+    Result:=L[L.Count-1];
+  finally
+    L.Free;
+  end;
+end;
+
+procedure TdmMessaging.RegisterSubscriptionExecute(Sender: TObject; const Params: TJSONData; out Res: TJSONData);
+
+var
+  Parms: TJSONArray absolute params;
+  aToken : UTF8String;
+
+begin
+  If Parms.Count<>1 then
+    Raise Exception.Create('Invalid param count');
+  if Parms[0].JSONType=JTString then
+    // FCM token
+    aToken:=Parms[0].AsString
+  else if Parms[0].JSONType=jtObject then
+    aToken:=Parms[0].AsJSON
+  else
+    Raise Exception.Create('Invalid param type for token');
+  SaveToken(aToken);
+  Res:=TJSONBoolean.Create(True);
+end;
+
+procedure TdmMessaging.SendMessage(Msg : TNotificationmessage);
+
+var
+  Sender : TFCMClient;
+  aConfig, aToken : String;
+
+begin
+  aToken:=LoadLastToken;
+  Sender:=TFCMClient.Create(Self);
+  try
+    aConfig:=ChangeFileExt(paramstr(0),'-serviceaccount.json');
+    Sender.LogFile:=ChangeFileExt(paramstr(0),'.log');
+    Sender.InitServiceAccount(aConfig,'');
+    Sender.OnNewBearerToken:=@HandleNewAccessToken;
+    if FileExists(AccessTokenFile) then
+      Sender.BearerToken.LoadFromFile(AccessTokenFile);
+    Sender.Send(Msg,aToken);
+  finally
+    Sender.Free;
+  end;
+end;
+
+procedure TdmMessaging.SendNotificationExecute(Sender: TObject; const Params: TJSONData; out Res: TJSONData);
+
+var
+  Parms: TJSONArray absolute params;
+  Obj : TJSONObject;
+  Msg : TNotificationMessage;
+
+begin
+  If Parms.Count<>1 then
+    Raise Exception.Create('Invalid param count');
+  if Parms[0].JSONType<>jtObject then
+    Raise Exception.Create('Invalid notification');
+  Obj:=Parms.Objects[0];
+  Msg:=TNotificationMessage.Create;
+  try
+    Msg.Title:=Obj.Get('title',Msg.Title);
+    Msg.Body:=Obj.Get('body',Msg.Body);
+    Msg.Image:=Obj.Get('image',Msg.Image);
+    SendMessage(Msg);
+  finally
+    Msg.Free;
+  end;
+end;
+
+initialization
+  JSONRPCHandlerManager.RegisterDatamodule(TdmMessaging, 'Messaging');
+end.
+

+ 15 - 0
packages/fcl-web/examples/fcm/server/module.rpc.lfm

@@ -0,0 +1,15 @@
+object rpcModule: TrpcModule
+  OldCreateOrder = False
+  DispatchOptions = [jdoSearchRegistry, jdoSearchOwner, jdoJSONRPC1, jdoJSONRPC2, jdoRequireClass, jdoNotifications, jdoAllowAPI, jdoCacheAPI]
+  CORS.Enabled = True
+  CORS.Options = [coAllowCredentials, coEmptyDomainToOrigin]
+  CORS.AllowedMethods = 'GET, PUT, POST, OPTIONS, HEAD'
+  CORS.AllowedOrigins = '*'
+  CORS.AllowedHeaders = 'x-requested-with, content-type, authorization'
+  CORS.MaxAge = 0
+  APIRequestName = 'API'
+  Height = 179
+  HorizontalOffset = 471
+  VerticalOffset = 419
+  Width = 409
+end

+ 45 - 0
packages/fcl-web/examples/fcm/server/module.rpc.pp

@@ -0,0 +1,45 @@
+{
+    This file is part of the Free Component Library
+    Copyright (c) 2024 by Michael Van Canneyt [email protected]
+
+    JSON-RPC dispatcher for webclient.
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+unit module.rpc;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, HTTPDefs, fpHTTP, fpWeb, fpjsonrpc, webjsonrpc;
+
+type
+
+  { TrpcModule }
+
+  TrpcModule = class(TJSONRPCModule)
+  private
+
+  public
+
+  end;
+
+var
+  rpcModule: TrpcModule;
+
+implementation
+
+{$R *.lfm}
+
+initialization
+  RegisterHTTPModule('RPC', TrpcModule);
+end.
+

+ 44 - 0
packages/fcl-web/examples/fcm/webclient/index.html

@@ -0,0 +1,44 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>FCM Push notification demo</title>
+  <link href="bulma.min.css" rel="stylesheet">
+  <script src="firebase-app-compat.js"></script>
+  <script src="firebase-messaging-compat.js"></script>
+  <script src="config.js"></script>
+  <script src="webclient.js"></script>
+
+</head>
+<body>
+  <div class="container">
+    <div class="box">
+      <h3 class="title is-3">FCM Push notification demo</h3>
+      <div class="field">
+        <label class="label">Message to send:</label>
+        <div class="control">
+          <input id="edtMessage" class="input" type="text" placeholder="Message to send">
+        </div>
+      </div>  <!-- .field -->
+
+      <div class="field is-grouped">
+        <div class="control">
+          <button id="btnSend" class="button is-link" disabled>Send</button>
+        </div>
+        <div class="control">
+          <button id="btnRegister" class="button is-link is-light">Register</button>
+        </div>
+      </div> <!-- .field -->
+    </div> <!-- .box -->
+    <div id="pnlToken" class="box is-hidden">
+      <p>Your token: <span id="lblToken">?</span> <p>
+    </div> <!-- .box -->
+
+  </div> <!-- .container -->
+  <script>
+    rtl.run();
+  </script>
+  <div id="pasjsconsole"></div>
+</body>
+</html>

+ 16 - 0
packages/fcl-web/examples/fcm/webclient/module.messagingservice.lfm

@@ -0,0 +1,16 @@
+object RPCModule: TRPCModule
+  OnCreate = DataModuleCreate
+  OldCreateOrder = False
+  Height = 150
+  HorizontalOffset = 442
+  VerticalOffset = 209
+  Width = 150
+  object Client: TPas2jsRPCClient
+    URL = 'http://localhost:8910/RPC'
+    Options = []
+    BatchTimeout = 100
+    JSONRPCversion = '2.0'
+    Left = 56
+    Top = 48
+  end
+end

+ 54 - 0
packages/fcl-web/examples/fcm/webclient/module.messagingservice.pp

@@ -0,0 +1,54 @@
+{
+    This file is part of the Free Component Library
+    Copyright (c) 2024 by Michael Van Canneyt [email protected]
+
+    FCM Messaging demo - web client : RPC client
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+unit module.messagingservice;
+
+{$mode ObjFPC}
+
+interface
+
+uses
+  Classes, SysUtils, fprpcclient, service.messagingserver;
+
+type
+
+  { TRPCModule }
+
+  TRPCModule = class(TDataModule)
+    Client: TPas2jsRPCClient;
+    procedure DataModuleCreate(Sender: TObject);
+  private
+    FService: TMessagingService;
+  public
+    Property Service : TMessagingService Read FService;
+  end;
+
+var
+  RPCModule: TRPCModule;
+
+implementation
+
+{$R *.lfm}
+
+{ TRPCModule }
+
+procedure TRPCModule.DataModuleCreate(Sender: TObject);
+begin
+  FService:=TMessagingService.Create(Self);
+  FService.RPCClient:=Client;
+end;
+
+end.
+

+ 97 - 0
packages/fcl-web/examples/fcm/webclient/service.messagingserver.pp

@@ -0,0 +1,97 @@
+{
+    This file is part of the Free Component Library
+    Copyright (c) 2024 by Michael Van Canneyt [email protected]
+
+    FCM Messaging demo - web client : RPC client definition.
+    This file is automatically generated by the RPC Client context menu in Lazarus.
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+Unit service.messagingserver;
+
+{$MODE ObjFPC}
+{$H+}
+
+interface
+
+uses js, fprpcclient;
+
+Type
+  
+  { --------------------------------------------------------------------
+    TMessagingService
+    --------------------------------------------------------------------}
+  
+  TMessagingService = Class(TRPCCustomService)
+  Protected
+    Function RPCClassName : string; override;
+  Public
+    Function SendNotification (Message : TJSObject; aOnSuccess : TJSValueResultHandler = Nil; aOnFailure : TRPCFailureCallBack = Nil) : NativeInt;
+    Function RegisterSubscription (Token : String; aOnSuccess : TJSValueResultHandler = Nil; aOnFailure : TRPCFailureCallBack = Nil) : NativeInt;
+  end;
+  
+  implementation
+  
+  
+  { --------------------------------------------------------------------
+    TMessagingService
+    --------------------------------------------------------------------}
+  
+  
+  Function TMessagingService.RPCClassName : string;
+  
+  begin
+    Result:='Messaging';
+  end;
+  
+  
+  Function TMessagingService.SendNotification (Message : TJSObject; aOnSuccess : TJSValueResultHandler = Nil; aOnFailure : TRPCFailureCallBack = Nil) : NativeInt;
+  
+    Procedure DoSuccess(Sender : TObject; const aResult : JSValue);
+    
+    begin
+      If Assigned(aOnSuccess) then
+        aOnSuccess(JSValue(aResult))
+    end;
+  
+  Var
+    _Params : JSValue;
+  
+  begin
+    StartParams;
+    AddParam('Message',Message);
+    _Params:=EndParams;
+    Result:=ExecuteRequest(RPCClassName,'SendNotification',_Params,@DoSuccess,aOnFailure);
+  end;
+  
+  
+  Function TMessagingService.RegisterSubscription (Token : String; aOnSuccess : TJSValueResultHandler = Nil; aOnFailure : TRPCFailureCallBack = Nil) : NativeInt;
+  
+    Procedure DoSuccess(Sender : TObject; const aResult : JSValue);
+    
+    begin
+      If Assigned(aOnSuccess) then
+        aOnSuccess(JSValue(aResult))
+    end;
+  
+  Var
+    _Params : JSValue;
+  
+  begin
+    StartParams;
+    AddParam('Token',Token);
+    _Params:=EndParams;
+    Result:=ExecuteRequest(RPCClassName,'RegisterSubscription',_Params,@DoSuccess,aOnFailure);
+  end;
+  
+  
+  
+  
+  end.

+ 117 - 0
packages/fcl-web/examples/fcm/webclient/webclient.lpi

@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="12"/>
+    <General>
+      <Flags>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+        <MainUnitHasScaledStatement Value="False"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <Title Value="webclient"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <CustomData Count="5">
+      <Item0 Name="BrowserConsole" Value="1"/>
+      <Item1 Name="MaintainHTML" Value="1"/>
+      <Item2 Name="Pas2JSProject" Value="1"/>
+      <Item3 Name="PasJSLocation" Value="$NameOnly($(ProjFile))"/>
+      <Item4 Name="PasJSWebBrowserProject" Value="1"/>
+    </CustomData>
+    <BuildModes>
+      <Item Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+    </RunParams>
+    <RequiredPackages>
+      <Item>
+        <PackageName Value="pas2jscomponents"/>
+      </Item>
+    </RequiredPackages>
+    <Units>
+      <Unit>
+        <Filename Value="webclient.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="index.html"/>
+        <IsPartOfProject Value="True"/>
+        <CustomData Count="1">
+          <Item0 Name="PasJSIsProjectHTMLFile" Value="1"/>
+        </CustomData>
+      </Unit>
+      <Unit>
+        <Filename Value="firebaseapp.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="firebase-messaging-sw.js"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="module.messagingservice.pp"/>
+        <IsPartOfProject Value="True"/>
+        <ComponentName Value="RPCModule"/>
+        <HasResources Value="True"/>
+        <ResourceBaseClass Value="DataModule"/>
+        <CustomData Count="1">
+          <Item0 Name="Client_filename" Value="/home/michael/source/FCM/webclient/service.messagingserver.pp"/>
+        </CustomData>
+      </Unit>
+      <Unit>
+        <Filename Value="service.messagingserver.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target FileExt=".js">
+      <Filename Value="webclient"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <UnitOutputDirectory Value="js"/>
+    </SearchPaths>
+    <Parsing>
+      <SyntaxOptions>
+        <AllowLabel Value="False"/>
+        <UseAnsiStrings Value="False"/>
+        <CPPInline Value="False"/>
+      </SyntaxOptions>
+    </Parsing>
+    <CodeGeneration>
+      <TargetOS Value="browser"/>
+    </CodeGeneration>
+    <Linking>
+      <Debugging>
+        <GenerateDebugInfo Value="False"/>
+        <UseLineInfoUnit Value="False"/>
+      </Debugging>
+    </Linking>
+    <Other>
+      <CustomOptions Value="-Jeutf-8 -Jirtl.js -Jc -Jminclude"/>
+      <CompilerPath Value="$(pas2js)"/>
+    </Other>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions>
+      <Item>
+        <Name Value="EAbort"/>
+      </Item>
+      <Item>
+        <Name Value="ECodetoolError"/>
+      </Item>
+      <Item>
+        <Name Value="EFOpenError"/>
+      </Item>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 190 - 0
packages/fcl-web/examples/fcm/webclient/webclient.lpr

@@ -0,0 +1,190 @@
+{
+    This file is part of the Free Component Library
+    Copyright (c) 2024 by Michael Van Canneyt [email protected]
+
+    FCM Messaging demo - web client
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+program webclient;
+
+{$mode objfpc}
+{$externalclasses}
+
+uses
+  BrowserConsole, JS, Classes, SysUtils, weborworker, Web, browserapp, firebaseapp,
+  module.messagingservice, service.messagingserver, fprpcclient;
+
+Type
+
+  { TDemoApp }
+
+  TDemoApp = class(TBrowserApplication)
+  private
+    procedure HandleReceivedMessage(aMessage: TJSObject);
+    procedure HaveToken(aToken: string);
+    procedure requestPermission;
+    procedure SendToken(aToken: string);
+    procedure ShowToken(aToken: string);
+  Public
+    pnlToken : TJSHTMLElement;
+    lblToken : TJSHTMLElement;
+    edtMessage : TJSHTMLInputElement;
+    btnSend: TJSHTMLButtonElement;
+    btnRegister:TJSHTMLButtonElement;
+    App : TFirebaseApp;
+    Reg: weborworker.TJSServiceWorkerRegistration;
+    Procedure DoRun; override;
+    procedure handleregister(event : TJSEvent); async;
+    procedure handlesend(event : TJSEvent); async;
+  end;
+
+var
+  Application : TDemoApp;
+  config : TJSObject; external name 'firebaseConfig';
+
+Const
+  TheVAPIDKey = 'The VAPID key for your FCM application';
+
+{ TDemoApp }
+
+
+procedure TDemoApp.HandleReceivedMessage(aMessage: TJSObject);
+begin
+  if assigned(aMessage) then
+    console.debug('Message received: ',aMessage);
+end;
+
+procedure TDemoApp.DoRun;
+
+
+begin
+  RPCModule:=TRPCModule.Create(Self);
+  pnlToken:=GetHTMLElement('pnlToken');
+  lblToken:=GetHTMLElement('lblToken');
+  edtMessage:=TJSHTMLInputElement(GetHTMLElement('edtMessage'));
+  btnSend:=TJSHTMLButtonElement(GetHTMLElement('btnSend'));
+  btnSend.addEventListener('click',@HandleSend);
+  btnRegister:=TJSHTMLButtonElement(GetHTMLElement('btnRegister'));
+  btnRegister.addEventListener('click',@HandleRegister);
+  Writeln('Initializing application...');
+  App:=Firebase.initializeApp(config);
+  App.messaging.onMessage(@HandleReceivedMessage);
+  Window.Navigator.serviceWorker.register('firebase-messaging-sw.js')._then(function (js : JSValue) :JSValue
+    begin
+    reg:=weborworker.TJSServiceWorkerRegistration(js);
+    if assigned(Reg) then
+      Writeln('Registered service worker...')
+    end,function (js : JSValue) :JSValue
+    begin
+      Writeln('Unable to register service worker')
+    end);
+end;
+
+procedure TDemoApp.ShowToken(aToken : string);
+begin
+  pnlToken.classlist.remove('is-hidden');
+  lblToken.innerText:=aToken;
+  Writeln(aToken);
+end;
+
+procedure TDemoApp.SendToken(aToken : string);
+
+  procedure DoOK(aResult: JSValue);
+  begin
+    Writeln('Registered token on server');
+  end;
+
+  procedure DoFail(Sender: TObject; const aError: TRPCError);
+  begin
+    Writeln('Failed to register token on server: '+aError.Message);
+  end;
+
+begin
+  Writeln('Sending token to server: ',aToken);
+  RPCModule.Service.RegisterSubscription(aToken,@DoOK,@DoFail);
+
+end;
+
+procedure TDemoApp.HaveToken(aToken : string);
+
+begin
+  Showtoken(aToken);
+  Sendtoken(aToken);
+  btnSend.disabled:=False;
+  btnRegister.disabled:=False;
+end;
+
+procedure TDemoApp.requestPermission;
+
+ function onpermission (permission : jsvalue) : jsvalue;
+
+ var
+   token : string;
+
+ begin
+   if (permission='granted') then
+     begin
+     writeln('Notification permission granted.');
+     handleregister(nil);
+     end;
+ end;
+
+begin
+  Writeln('Requesting permission...');
+  TJSNotification.requestPermission()._then(@OnPermission)
+end;
+
+procedure TDemoApp.handleregister(event: TJSEvent);
+
+var
+  Token : string;
+  opt : TMessagingGetTokenOptions;
+
+begin
+  opt:=TMessagingGetTokenOptions.New;
+  opt.serviceworkerRegistration:=self.Reg;
+  opt.vapidKey:=TheVAPIDKey;
+  Token:=Await(App.messaging.getToken(opt));
+  if (token='') then
+    RequestPermission
+  else
+    HaveToken(token);
+end;
+
+procedure TDemoApp.handlesend(event: TJSEvent);
+
+  procedure DoOK(aResult: JSValue);
+  begin
+    Writeln('Message transferred to server for sending');
+  end;
+
+  procedure DoFail(Sender: TObject; const aError: TRPCError);
+  begin
+    Writeln('Failed to transfer message to server for sending: '+aError.Message);
+  end;
+
+var
+  Msg : TJSObject;
+
+begin
+  Msg:=New([
+    'title','Free Pascal FCM demo',
+    'body',edtMessage.Value,
+    'image','https://www.freepascal.org/favicon.png'
+  ]);
+  RPCModule.Service.SendNotification(Msg,@DoOK,@DoFail);
+end;
+
+begin
+  Application:=TDemoApp.Create(Nil);
+  Application.Initialize;
+  Application.Run;
+end.

+ 15 - 0
packages/fcl-web/fpmake.pp

@@ -62,6 +62,7 @@ begin
     P.SourcePath.Add('src/hpack');
     P.SourcePath.Add('src/hpack');
     P.SourcePath.Add('src/restbridge');
     P.SourcePath.Add('src/restbridge');
     P.SourcePath.Add('src/websocket');
     P.SourcePath.Add('src/websocket');
+    P.SourcePath.Add('src/fcm');
     T:=P.Targets.addUnit('fpmimetypes.pp');
     T:=P.Targets.addUnit('fpmimetypes.pp');
 
 
     T:=P.Targets.AddUnit('httpdefs.pp');
     T:=P.Targets.AddUnit('httpdefs.pp');
@@ -518,6 +519,20 @@ begin
       AddUnit('fphttpclientpool');
       AddUnit('fphttpclientpool');
       end;
       end;
     P.NamespaceMap:='namespaces.lst';
     P.NamespaceMap:='namespaces.lst';
+
+    T:=P.Targets.AddUnit('fpfcmstrings.pp');
+    T:=P.Targets.AddUnit('fpfcmtypes.pp');
+    With T.Dependencies do
+      begin
+      AddUnit('fpjwt');
+      AddUnit('fpfcmstrings');
+      end;
+    T:=P.Targets.AddUnit('fpfcmsender.pp');
+    With T.Dependencies do
+      begin
+      AddUnit('fpfcmstrings');
+      AddUnit('fpfcmtypes');
+      end;
       
       
 end;
 end;
     
     

+ 3 - 0
packages/fcl-web/namespaced/Fcm.Sender.pp

@@ -0,0 +1,3 @@
+{$DEFINE FPC_DOTTEDUNITS}
+unit Fcm.Sender;
+{$i fpfcmsender.pp}

+ 3 - 0
packages/fcl-web/namespaced/Fcm.Strings.pp

@@ -0,0 +1,3 @@
+{$DEFINE FPC_DOTTEDUNITS}
+unit Fcm.Strings;
+{$i fpfcmstrings.pp}

+ 3 - 0
packages/fcl-web/namespaced/Fcm.Types.pp

@@ -0,0 +1,3 @@
+{$DEFINE FPC_DOTTEDUNITS}
+unit Fcm.Types;
+{$i fpfcmtypes.pp}

+ 3 - 0
packages/fcl-web/namespaces.lst

@@ -100,3 +100,6 @@ src/jwt/fpjwaes256.pp=namespaced/Jwt.Jwa.Es256.pp
 src/jwt/fpjwasha256.pp=namespaced/Jwt.Jwa.Sha256.pp
 src/jwt/fpjwasha256.pp=namespaced/Jwt.Jwa.Sha256.pp
 src/jwt/fpjwt.pp=namespaced/Jwt.Types.pp
 src/jwt/fpjwt.pp=namespaced/Jwt.Types.pp
 src/jwt/fpoauth2.pp=namespaced/Jwt.Oauth2.pp
 src/jwt/fpoauth2.pp=namespaced/Jwt.Oauth2.pp
+src/fcm/fpfcmsender.pp=Fcm.Sender.pp
+src/fcm/fpfcmstrings.pp=Fcm.Strings.pp
+src/fcm/fpfcmtypes.pp=Fcm.Types.pp

+ 550 - 0
packages/fcl-web/src/fcm/fpfcmsender.pp

@@ -0,0 +1,550 @@
+{
+    This file is part of the Free Component Library
+    Copyright (c) 2024 by Michael Van Canneyt [email protected]
+
+    FCM (Firebase Cloud Messaging) - Component to send a message through FCM.
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+unit fpfcmsender;
+
+{$mode ObjFPC}{$H+}
+{$modeswitch typehelpers}
+{$modeswitch advancedrecords}
+
+interface
+
+uses
+  Classes, SysUtils, fpjson, fpjwt, fpfcmtypes, fpwebclient, types;
+
+type
+  TFCMErrorStage = (esConfig, esAccessToken, esPost);
+
+  { TFCMErrorStageHelper }
+
+  TFCMErrorStageHelper = type helper for TFCMErrorStage
+    function ToString: string;
+  end;
+
+  { TFCMError }
+
+  TFCMError = record
+    Content: string;
+    Message: string;
+    Stage: TFCMErrorStage;
+    constructor Create(const aStage: TFCMErrorStage; const aContent, aMessage: string);
+  end;
+
+
+
+  { TFCMResponse }
+
+  TFCMResponse = record
+    Response: string;
+    Headers : TStringDynArray;
+    constructor Create(const aResponse: string; aHeaders : TStringDynArray);
+  end;
+
+  TFCMResponseEvent = procedure(Sender: TObject; const aResponse: TFCMResponse) of object;
+  TFCMBearerTokenEvent = procedure(Sender: TObject; const aToken: TBearerToken) of object;
+  TFCMErrorEvent = procedure(Sender: TObject; const aError: TFCMError) of object;
+
+
+  { TFCMClient }
+
+  TFCMClient = class(TComponent)
+  private
+    FBearerToken: TBearerToken;
+    FLogFile: String;
+    FOnNewBearerToken: TFCMBearerTokenEvent;
+    FServiceAccount: TServiceAccountData;
+    FOnError: TFCMErrorEvent;
+    FOnResponse: TFCMResponseEvent;
+    FWebClient: TAbstractWebClient;
+    function ExtractContentType(const aHeader: String): string;
+    function GetWebClient: TAbstractWebClient;
+    procedure SetBearerToken(AValue: TBearerToken);
+    procedure SetWebClient(AValue: TAbstractWebClient);
+  protected
+    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
+    //
+    // General stuff
+    // Get send endpoint
+    function GetSendEndPoint(const aProjectID: string): UTF8string;
+    // Create JWT token to get access token. Called by GetAccessToken
+    function GenerateJWT: string;
+    // Get an access token.
+    function GetAccessToken: Boolean;
+    //
+    // HTTP Transport
+    //
+    // Override these if you don't want to use TAbstractWebClient.
+    // Must set bearertoken.  Must raise exception on error.
+    procedure FetchAccessToken(const aUrl, aJWT: string); virtual;
+    // Must raise an exception on error
+    procedure HandlePost(const aURL, aJSON: UTF8String); virtual;
+    //
+    // Default HTTP implementation using WebClient
+    //
+    // Create webclient if needed.
+    function CreateWebClient: TAbstractWebClient; virtual;
+    // Handle WebClient token response
+    procedure HandleTokenResponse(aResponse: TWebClientResponse);
+    // Handle WebClient send response
+    procedure HandlePostResponse(aResponse: TWebClientResponse);
+    //
+    // Events & Errors
+    //
+    // Convert exception to error string.
+    function ExceptionToString(E: Exception): string; virtual;
+    // Handle error. If this returns true, the error was handled. If false, caller must raise an exception.
+    function HandleError(const aError: TFCMError): Boolean; virtual;
+    // Called when new bearer token is available.
+    procedure HandleNewBearerToken; virtual;
+    // Call event handler for bearer token.
+    procedure HandleAccessToken; virtual;
+    // Call response event handler
+    function HandleResponse(const aResponse: TFCMResponse) : boolean; virtual;
+    // Send the message
+    function Post(const aJSON: UTF8String): Boolean;
+  public
+    Constructor Create(aOwner : TComponent); override;
+    Destructor Destroy; override;
+
+    // These calls will raise an exception, regardless of OnError being set.
+    Procedure InitServiceAccount(const aFileName: string; aRoot: TJSONStringType);
+    Procedure InitServiceAccount(const aJSON : TStream; aRoot: TJSONStringType);
+    Procedure InitServiceAccount(const aJSON : TJSONStringType; aRoot: TJSONStringType);
+    Procedure InitServiceAccount(const aJSON : TJSONObject);
+    // Web client to use for requests. A default is created if none is set.
+    Property WebClient : TAbstractWebClient Read GetWebClient Write SetWebClient;
+    // Serialize and send.
+    function Send(aMsg : TNotificationMessage; aRecipient : UTF8String) : Boolean;
+    // Serialize and send to multiple recipeints
+    function Send(aMsg : TNotificationMessage; aRecipients : Array of UTF8String) : Boolean;
+    // Current bearer token. You can set this if you stored it somewhere
+    property BearerToken: TBearerToken read FBearerToken write SetBearerToken;
+    // Service account data. This will be used to get a bearer token.
+    property ServiceAccount: TServiceAccountData read FServiceAccount;
+    // Set this if you wish to handle errors. If not set, an exception is raised on error.
+    property OnError: TFCMErrorEvent read FOnError write FOnError;
+    // Called when a new access token was received.
+    property OnNewBearerToken : TFCMBearerTokenEvent read FOnNewBearerToken write FOnNewBearerToken;
+    // Called with send response.
+    property OnResponse: TFCMResponseEvent read FOnResponse write FOnResponse;
+    // Set this if you want a log of the HTTP communications with google FCM servers
+    Property LogFile: String Read FLogFile Write FLogFile;
+  end;
+
+implementation
+
+uses dateutils, httpprotocol, fpfcmstrings, fpjwarsa, fppem;
+
+
+{ TFCMErrorStageHelper }
+
+function TFCMErrorStageHelper.ToString: string;
+begin
+  WriteStr(Result,Self);
+end;
+
+{ TFCMError }
+
+constructor TFCMError.Create(const aStage: TFCMErrorStage; const aContent, aMessage: string);
+begin
+  Stage:=aStage;
+  Content:=aContent;
+  Message:=aMessage;
+end;
+
+{ TSenderResponse }
+
+constructor TFCMResponse.Create(const aResponse: string; aHeaders: TStringDynArray);
+begin
+  Response:=aResponse;
+end;
+
+{ TFCMClient }
+
+procedure TFCMClient.SetWebClient(AValue: TAbstractWebClient);
+begin
+  if FWebClient=AValue then Exit;
+  if Assigned(FWebCLient) and (csSubComponent in FWebCLient.ComponentStyle) then
+    FreeAndNil(FWebClient);
+  FWebClient:=AValue;
+end;
+
+function TFCMClient.ExceptionToString(E: Exception): string;
+begin
+  With E do
+    Result:=Format(SErrorMessage,[ClassName,Message])
+end;
+
+function TFCMClient.HandleError(const aError: TFCMError) : Boolean;
+begin
+  Result:=Assigned(FOnError);
+  if Result then
+    FOnError(Self,aError);
+end;
+
+function TFCMClient.GetSendEndPoint(const aProjectID: string): UTF8string;
+
+begin
+  Result:=Format(SFCMSendURL,[aProjectID]);
+end;
+
+procedure TFCMClient.HandlePost(const aURL,aJSON: UTF8String);
+
+var
+  Request : TWebClientRequest;
+  Response : TWebClientResponse;
+
+begin
+  Response:=Nil;
+  Request:=WebClient.CreateRequest;
+  try
+    Request.Headers.Values[HeaderAccept]:=SContentTypeApplicationJSON;
+    Request.Headers.Values[HeaderContentType]:=SContentTypeApplicationJSON;
+    Request.Headers.Values[HeaderAuthorization]:='Bearer '+BearerToken.access_token;
+    Request.SetContentFromString(AJSON);
+    Response:=WebClient.ExecuteRequest('POST',aURL,Request);
+    HandlePostResponse(Response);
+  finally
+    Request.Free;
+    Response.Free;
+  end;
+end;
+
+function TFCMClient.ExtractContentType(const aHeader : String) : string;
+
+var
+  P : Integer;
+
+begin
+  // Handle things like:
+  // application/json ; charset=UTF8
+  P:=Pos(';',aHeader);
+  if P>0 then
+    Result:=Trim(Copy(aHeader,1,P-1))
+  else
+    Result:=aHeader;
+end;
+
+procedure TFCMClient.HandlePostResponse(aResponse : TWebClientResponse);
+
+var
+  Resp : TFCMResponse;
+  CT : String;
+  Headers : TStringDynArray;
+  I : Integer;
+
+begin
+  Headers:=[];
+  if not (aResponse.StatusCode in [200,204]) then
+    Raise EFCM.CreateFmt(SErrInvalidResponseStatus,[aResponse.StatusCode,aResponse.StatusText,aResponse.GetContentAsString]);
+  CT:=ExtractContentType(aResponse.Headers.Values[HeaderContentType]);
+  if not SameText(CT,SContentTypeApplicationJSON) then
+    Raise EFCM.CreateFmt(SErrInvalidResponseContentType,[CT]);
+  if Assigned(FOnResponse) then
+    begin
+    SetLength(Headers,aResponse.Headers.Count);
+    For I:=0 to Length(Headers)-1 do
+     Headers[I]:=aResponse.Headers[i];
+    Resp:=TFCMResponse.Create(aResponse.GetContentAsString,Headers);
+    HandleResponse(Resp);
+    end;
+end;
+
+procedure TFCMClient.HandleTokenResponse(aResponse : TWebClientResponse);
+
+var
+  JSONData : TJSONData;
+  Ct : String;
+
+begin
+  if not (aResponse.StatusCode in [200,204]) then
+    Raise EFCM.CreateFmt(SErrInvalidResponseStatus,[aResponse.StatusCode,aResponse.StatusText,aResponse.GetContentAsString]);
+  CT:=ExtractContentType(aResponse.Headers.Values[HeaderContentType]);
+  if not SameText(CT,SContentTypeApplicationJSON) then
+    Raise EFCM.CreateFmt(SErrInvalidResponseContentType,[CT]);
+  JSONData:=GetJSON(aResponse.Content);
+  try
+    if not (JSONData is TJSONObject) then
+      Raise EFCM.Create(SErrInvalidJSONResponse);
+    FBearerToken.LoadFromJSON(JSONData as TJSONObject);
+    FBearerToken.TokenDateTime:=Now;
+  finally
+    JSONData.Free;
+  end;
+end;
+
+procedure TFCMClient.FetchAccessToken(const aUrl, aJWT: string);
+
+var
+  Request : TWebClientRequest;
+  Response : TWebClientResponse;
+
+begin
+  Response:=Nil;
+  Request:=WebClient.CreateRequest;
+  try
+    Request.SetContentFromString(Format(SFCMAccessTokenQuery,[HTTPEncode(SFCMGrantType),HTTPEncode(AJWT)]));
+    Request.Headers.Values[HeaderContentType]:=SContentTypeApplicationFormUrlEncoded;
+    Response:=WebClient.ExecuteRequest('POST',aUrl,Request);
+    HandleTokenResponse(Response);
+  finally
+    Request.Free;
+    Response.Free;
+  end;
+end;
+
+
+function TFCMClient.HandleResponse(const aResponse: TFCMResponse): boolean;
+begin
+  Result:=Assigned(FOnResponse);
+  if Result then
+    FOnResponse(Self, AResponse);
+end;
+
+procedure TFCMClient.Notification(AComponent: TComponent; Operation: TOperation);
+begin
+  inherited Notification(AComponent, Operation);
+  if (Operation=opRemove) and (aComponent=FWebClient) then
+    FWebClient:=Nil;
+end;
+
+procedure TFCMClient.HandleAccessToken;
+begin
+
+end;
+
+function TFCMClient.GenerateJWT: string;
+
+var
+  LJWT: TGoogleJWT;
+  Signer : TJWTSignerRS256;
+  Key : TJWTKey;
+  pkt : TPrivateKeyType;
+  pk,ec : TBytes;
+
+begin
+  Result:='';
+  try
+    if not FServiceAccount.IsValid then
+      Raise EFCM.Create(SErrInvalidServiceData);
+    Signer:=Nil;
+    LJWT:=TGoogleJWT.Create;
+    try
+      LJWT.JOSE.Alg := 'RS256';
+      LJWT.JOSE.Typ := 'JWT';
+      LJWT.Claims.Aud := SFCMAudience;
+      LJWT.Claims.Iss:=FServiceAccount.client_email;
+      LJWT.Claims.iat:=DateTimeToUnix(Now, False);
+      LJWT.Claims.exp:=DateTimeToUnix(IncHour(Now, 1), False);
+      LJWT.GoogleClaims.scope:=SFCMScopes;
+      Signer:=TJWTSignerRS256.Create;
+      PemLoadPrivateKeyAsDER(FServiceAccount.private_key,pk,ec,pkt);
+      if pk=Nil then
+        Raise EFCM.Create(SErrInvalidPrivateKey);
+      Key:=TJWTKey.Create(pk);
+      LJWT.Signature:=Signer.CreateSignature(LJWT,Key);
+      Result:=LJWT.AsString;
+    finally
+      LJWT.Free;
+      Signer.Free;
+    end;
+  except
+    on E: Exception do
+      if not HandleError(TFCMError.Create(esConfig,'',ExceptionToString(E))) then
+        Raise;
+  end;
+end;
+
+function TFCMClient.CreateWebClient: TAbstractWebClient;
+
+begin
+  if not Assigned(DefaultWebClientClass) then
+    Raise EFCM.Create(SErrNoWebclientSet);
+  Result:=DefaultWebClientClass.Create(Self);
+  Result.SetSubComponent(True);
+  if Self.LogFile<>'' then
+    Result.LogFile:=Self.LogFile;
+end;
+
+function TFCMClient.GetWebClient: TAbstractWebClient;
+begin
+  if FWebClient=Nil then
+    FWebClient:=CreateWebClient;
+  Result:=FWebClient;
+end;
+
+procedure TFCMClient.SetBearerToken(AValue: TBearerToken);
+begin
+  if FBearerToken=AValue then Exit;
+  FBearerToken.Assign(AValue);
+end;
+
+procedure TFCMClient.HandleNewBearerToken;
+
+begin
+  If Assigned(FOnNewBearerToken) then
+    FOnNewBearerToken(Self,FBearerToken);
+end;
+
+function TFCMClient.GetAccessToken: Boolean;
+
+var
+  JWT: string;
+
+begin
+  JWT:=GenerateJWT;
+  try
+    Result:=JWT<>'';
+    FetchAccessToken(FServiceAccount.token_uri,JWT);
+    Result:=not BearerToken.IsExpired;
+    if Result then
+      HandleNewBearerToken
+    else
+      Raise EFCM.Create(SErrReceivedExpiredToken);
+  except
+    on E: Exception do
+      if not HandleError(TFCMError.Create(esAccessToken,JWT,ExceptionToString(E))) then
+          Raise;
+  end;
+end;
+
+procedure TFCMClient.InitServiceAccount(const aFileName: string; aRoot: TJSONStringType);
+
+var
+  F : TFileStream;
+
+begin
+  F:=TFileStream.Create(aFileName,fmOpenRead or fmShareDenyWrite);
+  try
+    InitServiceAccount(F,aRoot);
+  finally
+    F.Free;
+  end;
+end;
+
+procedure TFCMClient.InitServiceAccount(const aJSON: TStream; aRoot: TJSONStringType);
+
+var
+  Data : TJSONData;
+  Obj : TJSONObject absolute Data;
+  AccountObj : TJSONObject;
+
+begin
+  AccountObj:=nil;
+  Data:=GetJSON(aJSON);
+  try
+    if not (Data is TJSONObject) then
+      Raise EFCM.Create(SErrInvalidJSONServiceData);
+    if aRoot='' then
+      AccountObj:=Obj
+    else
+      if not Obj.Find(aRoot,AccountObj) then
+        Raise EFCM.CreateFmt(SErrNoServiceDataAt,[aRoot]);
+    FServiceAccount.LoadFromJSON(AccountObj);
+  finally
+    Data.Free;
+  end;
+end;
+
+procedure TFCMClient.InitServiceAccount(const aJSON: TJSONStringType; aRoot: TJSONStringType);
+
+var
+  S : TBytesStream;
+
+begin
+  S:=TBytesStream.Create(TEncoding.UTF8.GetAnsiBytes(aJSON));
+  try
+    InitServiceAccount(S,aRoot);
+  finally
+    S.Free;
+  end;
+end;
+
+procedure TFCMClient.InitServiceAccount(const aJSON: TJSONObject);
+begin
+  FServiceAccount.LoadFromJSON(aJSON);
+  If not FServiceAccount.IsValidData then
+    Raise EFCM.Create(SErrInvalidServiceData);
+end;
+
+function TFCMClient.Send(aMsg: TNotificationMessage; aRecipient: UTF8String): Boolean;
+begin
+  Result:=Send(aMsg,[aRecipient]);
+end;
+
+function TFCMClient.Send(aMsg: TNotificationMessage; aRecipients: array of UTF8String): Boolean;
+
+var
+  aJSON,aMsgJSON : TJSONObject;
+  I : integer;
+
+begin
+    Result:=True;
+    I:=Low(aRecipients);
+    While Result and (I<=High(aRecipients)) do
+      begin
+      aJSON:=TJSONObject.Create;
+      try
+        aMsgJSON:=TJSONObject.Create;
+        aMsg.Recipient:=aRecipients[i];
+        aMsg.RecipientType:=rtToken;
+        aMsg.ToJSON(aMsgJSON);
+        aJSON.Add('message',aMsgJSON);
+        Result:=Post(aJSON.AsJSON);
+      finally
+        aJSON.Free;
+      end;
+      Inc(I);
+      end;
+end;
+
+function TFCMClient.Post(const aJSON: UTF8String): Boolean;
+
+var
+  URL : UTF8String;
+
+begin
+  try
+    Result:=not BearerToken.IsExpired;
+    If not Result then
+      Result:=GetAccessToken;
+    if Result then
+      begin
+      URL:=GetSendEndPoint(ServiceAccount.project_id);
+      HandlePost(URL,aJSON)
+      end;
+  except
+    on E: Exception do
+      if not HandleError(TFCMError.Create(esPost,aJSON,ExceptionToString(E))) then
+        Raise;
+  end;
+end;
+
+constructor TFCMClient.Create(aOwner: TComponent);
+begin
+  inherited Create(aOwner);
+  FServiceAccount:=TServiceAccountData.Create;
+  FBearerToken:=TBearerToken.Create;
+end;
+
+destructor TFCMClient.Destroy;
+begin
+  FreeAndNil(FBearerToken);
+  FreeAndNil(FServiceAccount);
+  inherited Destroy;
+end;
+
+end.
+

+ 66 - 0
packages/fcl-web/src/fcm/fpfcmstrings.pp

@@ -0,0 +1,66 @@
+{
+    This file is part of the Free Component Library
+    Copyright (c) 2024 by Michael Van Canneyt [email protected]
+
+    FCM (Firebase Cloud Messaging) - Strings used in protocol and messages
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+
+unit fpfcmstrings;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+// Constants used in the protocol. Do not localize
+
+const
+  SFCMBaseURL = 'https://fcm.googleapis.com/v1';
+  SFCMSendURL = SFCMBaseURL + '/projects/%s/messages:send';
+
+  SFCMHTTPv1SendResourceProjectID = 'projectid';
+  SFCMHTTPv1SendResource = '/projects/{projectid}/messages:send';
+
+  SFCMGrantType = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
+  SContentTypeApplicationJSON = 'application/json';
+  SContentTypeApplicationFormUrlEncoded = 'application/x-www-form-urlencoded';
+
+  SFCMAccessTokenQuery = 'grant_type=%s&assertion=%s';
+  SFCMScopes = 'https://www.googleapis.com/auth/firebase.messaging';
+  SFCMAudience = 'https://oauth2.googleapis.com/token';
+
+// Constants uses in JSON reading/writing
+Const
+  keyClientID = 'client_id';
+  keyClientEmail = 'client_email';
+  keyPrivateKey = 'private_key';
+  keyProjectID = 'project_id';
+  keyAuthURI = 'auth_uri';
+  keyTokenURI = 'token_uri';
+
+
+resourcestring
+  // Messages, these can be localized.
+  SErrInvalidJSONServiceData = 'Invalid service account data in JSON';
+  SErrNoServiceDataAt = 'JSON contains no service account data at "%s"';
+  SErrInvalidServiceData = 'Service data is invalid';
+  SErrInvalidResponseStatus = 'Invalid HTTP response status: %d (%s).'#10'Extra info: %s';
+  SErrInvalidResponseContentType = 'Invalid HTTP content type: %s';
+  SErrInvalidJSONResponse = 'Invalid JSON data in HTTP response';
+  SErrNoWebclientSet = 'No webclient class was set, inclucde fphttpwebclient or another webclient implementation';
+  SErrReceivedExpiredToken = 'Received expired bearer token';
+  SErrInvalidPrivateKey = 'Invalid private key in service account';
+  SErrorMessage = 'Error %s: %s';
+
+implementation
+
+end.
+

+ 1497 - 0
packages/fcl-web/src/fcm/fpfcmtypes.pp

@@ -0,0 +1,1497 @@
+{
+    This file is part of the Free Component Library
+    Copyright (c) 2024 by Michael Van Canneyt [email protected]
+
+    FCM (Firebase Cloud Messaging) - Types
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+unit fpfcmtypes;
+
+{$mode ObjFPC}
+{$H+}
+
+
+interface
+
+uses
+  Classes, SysUtils, fpjson, fpjwt;
+
+Type
+  EFCM = Class(Exception)
+  end;
+
+  { TBearerToken }
+
+  TBearerToken = Class(TBaseJWT)
+  private
+    faccess_token: string;
+    fexpires_in: Integer;
+    fext_expires_in: Integer;
+    fid_token: string;
+    frefresh_token: string;
+    fscope: string;
+    FTokenDateTime: TDateTime;
+    ftoken_type: string;
+    function GetTokenDateTime: string;
+    procedure SetTokenDateTime(AValue: string);
+  Public
+//    function Parse(const AJSON: string): Boolean;
+    Procedure SaveToStream(aStream : TStream);
+    Procedure SaveToFile(Const aFileName : String);
+    Procedure LoadFromStream(aStream : TStream);
+    Procedure LoadFromFile(Const aFileName : String);
+    function IsValidData: Boolean;
+    Function IsExpired : Boolean;
+    Property TokenDateTime : TDateTime Read FTokenDateTime Write FTokenDateTime;
+  published
+    property access_token: string read faccess_token write faccess_token;
+    property expires_in: Integer read fexpires_in write fexpires_in;
+    property ext_expires_in: Integer read fext_expires_in write fext_expires_in;
+    property id_token: string read fid_token write fid_token;
+    property refresh_token: string read frefresh_token write frefresh_token;
+    property scope: string read fscope write fscope;
+    property token_type: string read ftoken_type write ftoken_type;
+    property token_dateTime: string read GetTokenDateTime Write SetTokenDateTime;
+  end;
+
+  { TServiceAccountData }
+
+  TServiceAccountData = Class(TBaseJWT)
+  private
+    FAuthURI: string;
+    FClientEmail: string;
+    FClientID: string;
+    FPrivateKey: string;
+    FProjectID: string;
+    FTokenURI: string;
+  Protected
+  public
+    function Parse(const AJSON: string): Boolean;
+    function IsValidData: Boolean;
+  Published
+    Property auth_uri: string Read FAuthURI Write FAuthURI;
+    Property client_email: string read FClientEmail  Write FClientEmail;
+    Property client_id: string Read FClientID Write FClientID;
+    Property IsValid: Boolean Read IsValidData;
+    Property private_key: string Read FPrivateKey Write FPrivateKey;
+    Property project_id: string Read FProjectID Write FProjectID;
+    Property token_uri: string Read FTokenURI Write FTokenURI;
+  end;
+
+  TMessageOption = (moLargeText,moLargeImage,moContent);
+  TMessageOptions = set of TMessageOption;
+
+  { TGoogleJWT }
+
+  { TGoogleClaims }
+
+  TGoogleClaims = class(TClaims)
+  private
+    fscope: string;
+  Published
+    property scope : string read fscope write fscope;
+  end;
+
+  TGoogleJWT = Class(TJWT)
+  private
+    function GetGoogleClaims: TGoogleClaims;
+  Protected
+    Function CreateClaims: TClaims; override;
+  Public
+    Property GoogleClaims : TGoogleClaims Read GetGoogleClaims;
+  end;
+
+  { TPlatformOptions }
+
+  // A helper class to ease streaming
+
+  { TJSONPersist }
+
+  TJSONPersist = class(TPersistent)
+  private
+  Protected
+    // All calls return true if the value was added, false if it was not added.
+    // Adds if avalue is nonempty string
+    Function JSONAdd(Obj : TJSONObject; const aKey,aValue : String) : Boolean;
+    // Adds if aValue.count>0
+    Function JSONAdd(Obj : TJSONObject; const aKey : String; aValue : TJSONObject) : Boolean;
+    // Adds if avalue <>0
+    Function JSONAdd(Obj : TJSONObject; const aKey : String; aValue : Integer) : Boolean;
+    // Adds if avalue <>0
+    Function JSONAdd(Obj : TJSONObject; const aKey : String; aValue : Int64) : Boolean;
+    // Adds if avalue = true
+    Function JSONAdd(Obj : TJSONObject; const aKey : String; aValue : Boolean) : Boolean;
+    // Get sub-object with name key, create if needed.
+    Function JSONGetSub(Obj : TJSONObject; const aKey : String) : TJSONObject;
+    // Adds as name : value key pairs.
+    Function JSONAddStrings(Obj : TJSONObject; aValue : TStrings) : Integer;
+    // Adds strings as array.
+    function JSONAddStringsRaw(Obj: TJSONObject; aKey: String; aValue: TStrings): Integer;
+  end;
+
+  { TPlatformConfig }
+
+  TPlatformConfig = class(TJSONPersist)
+  private
+    FCustomPayLoad: TJSONObject;
+  protected
+    procedure AddCustomPayload(aObject : TJSONObject);
+  Public
+    procedure Assign(Source: TPersistent); override;
+    constructor Create;virtual;
+    destructor Destroy; override;
+    procedure Clear; virtual;
+    procedure ToJSON(Obj : TJSONObject); virtual;
+    // Keys in this object will be added to the top-level platform-specific payload.
+    Property CustomPayLoad : TJSONObject Read FCustomPayLoad;
+  end;
+
+  { THeadersPlatformConfig }
+
+  THeadersPlatformConfig = class(TPlatformConfig)
+    FHeaders: TStrings;
+    procedure SetHeaders(AValue: TStrings);
+  Protected
+    function addHeaders(Obj: TJSONObject; asSub : Boolean = True) : Integer;
+  Public
+    procedure Assign(Source: TPersistent); override;
+    constructor Create; override;
+    destructor Destroy; override;
+    procedure Clear; override;
+  Published
+    property Headers : TStrings Read FHeaders Write SetHeaders;
+  end;
+
+  { TFCMOptions }
+
+  TFCMOptions = class(TPlatformConfig)
+  private
+    FAnalyticsLabel: String;
+  Public
+    procedure Assign(Source: TPersistent); override;
+    procedure ToJSON(Obj : TJSONObject); override;
+    function HaveData : Boolean; virtual;
+    procedure Clear; override;
+  Published
+    Property AnalyticsLabel : String Read FAnalyticsLabel Write FAnalyticsLabel;
+  end;
+
+  { TAppleFCMOptions }
+
+  TAppleFCMOptions = class(TFCMoptions)
+  private
+    FImage: String;
+    Public
+    procedure Assign(Source: TPersistent); override;
+    procedure ToJSON(Obj : TJSONObject); override;
+    function HaveData : Boolean; override;
+    procedure Clear; override;
+  Published
+    Property Image : String Read FImage Write FImage;
+  end;
+
+  { TAppleConfig }
+
+  TAppleConfig = class(THeadersPlatformConfig)
+  private
+    FAlert: string;
+    FAlertObject: TJSONObject;
+    Fattributestype: string;
+    FBadge: Integer;
+    fcategory: string;
+    Fdismissaldate: Int64;
+    FEvent: string;
+    FFCMOPtions: TAppleFCMOptions;
+    FFiltercriteria: string;
+    Finterruptionlevel: string;
+    fmutablecontent: integer;
+    Frelevancescore: string;
+    FSound: string;
+    FStaleDate: Int64;
+    Ftargetcontentid: string;
+    Fthreadid: string;
+    FTimeStamp: Int64;
+    function CreateFCMOptions: TAppleFCMOptions;
+    procedure SetFFCMoptions(AValue: TAppleFCMOptions);
+  protected
+    procedure AddPayLoad(Obj : TJSONObject); virtual;
+  Public
+    constructor Create; override;
+    destructor Destroy; override;
+    procedure Assign(Source: TPersistent); override;
+    procedure ToJSON(Obj : TJSONObject); override;
+    procedure Clear; override;
+    Property AlertObject : TJSONObject Read FAlertObject;
+  Published
+    Property Alert : string Read FAlert Write falert;
+    property Badge : Integer Read FBadge write fbadge;
+    Property ThreadID : string read Fthreadid Write Fthreadid;
+    property Category : string read fcategory write fcategory;
+    property MutableContent : integer Read fmutablecontent write fmutablecontent;
+    Property TargetContentID : string read Ftargetcontentid Write Ftargetcontentid;
+    Property InterruptionLevel : string read Finterruptionlevel Write Finterruptionlevel;
+    Property RelevanceScore : string read Frelevancescore Write Frelevancescore;
+    Property FilterCriteria : string read FFiltercriteria Write FFiltercriteria;
+    Property StaleDate : Int64 read FStaleDate Write FStaleDate;
+    property Timestamp : Int64 read FTimeStamp Write FTimeStamp;
+    property DismissalDate : Int64 read Fdismissaldate Write Fdismissaldate;
+    property Sound: string read FSound write FSound;
+    Property Event : string read FEvent Write FEvent;
+    Property AttributesType : string Read fattributestype Write Fattributestype;
+    property FCMoptions : TAppleFCMOptions Read FFCMOPtions Write SetFFCMoptions;
+  end;
+
+
+  { TAndroidConfig }
+
+  TAndroidMessageVisibility = (mvUnspecified,mvPrivate,mvPublic,mvSecret);
+  TAndroidNotificationPriority = (npUnspecified,npMin,npLog,npDefault,npHigh,npMax);
+  TAndroidMessagePriority = (mpUnspecified,mpNormal,mpHigh);
+
+  TAndroidConfig = class(TPlatformConfig)
+  private
+    Fbody: string;
+    fbodylocargs: TStrings;
+    fbodylockey: string;
+    FChannelID: string;
+    FClickAction: string;
+    FFCMoptions: TFCMOptions;
+    FCollapseKey: string;
+    fcolor: string;
+    FData: TJSONObject;
+    fdefaultlightsettings: boolean;
+    Fdefaultsound: boolean;
+    Fdefaultvibratetimings: boolean;
+    fdirectbootOK: Boolean;
+    Feventtime: string;
+    Ficon: string;
+    fimage: string;
+    Flightsettings: TJSONObject;
+    flocalonly: boolean;
+    fnotificationcount: integer;
+    FNotificationPriority: TAndroidNotificationPriority;
+    FOwnsData: boolean;
+    FPriority: TAndroidMessagePriority;
+    Frestrictedpackagename: string;
+    fsound: string;
+    fsticky: boolean;
+    ftag: string;
+    fticker: string;
+    Ftitle: string;
+    ftitlelocargs: TStrings;
+    ftitlelockey: string;
+    Fttl: string;
+    Fvibratetimings: TStrings;
+    FVisibility: TAndroidMessageVisibility;
+    procedure setbodylocargs(AValue: TStrings);
+    procedure SetData(AValue: TJSONObject);
+    procedure SetFCMOptions(AValue: TFCMOptions);
+    procedure settitlelocargs(AValue: TStrings);
+    procedure Setvibratetimings(AValue: TStrings);
+  Public
+    procedure Assign(Source: TPersistent); override;
+    constructor create; override;
+    destructor destroy; override;
+    procedure Clear; override;
+    procedure ToJSON(Obj : TJSONObject); override;
+    property data : TJSONObject Read FData Write SetData;
+    Property OwnsData : boolean read FOwnsData Write FOwnsData;
+    property lightsettings : TJSONObject Read Flightsettings;
+  Published
+    property ChannelID: string read FChannelID write FChannelID;
+    property CollapseKey : string read FCollapseKey Write FCollapseKey;
+    property Priority: TAndroidMessagePriority read FPriority write FPriority;
+    property TTL : string read Fttl Write FTTL;
+    Property RestrictedPackageName : string read Frestrictedpackagename Write Frestrictedpackagename;
+    property DirectBootOk : Boolean Read fdirectbootOK write fdirectbootOK;
+    property title : string read Ftitle write FTitle;
+    property Body : string read Fbody write Fbody;
+    property Icon : string read Ficon write Ficon;
+    property Color : string read fcolor write fcolor;
+    property Sound : string read fsound write fsound;
+    property Tag : string read ftag write ftag;
+    property ClickAction: string read FClickAction write FClickAction;
+    property BodyLocKey : string read fbodylockey write Fbodylockey;
+    property BodyLocArgs : TStrings read fbodylocargs write setbodylocargs;
+    property TitleLocKey : string read ftitlelockey write Ftitlelockey;
+    property TitleLocArgs : TStrings read ftitlelocargs write settitlelocargs;
+    property Ticker : string read fticker write Fticker;
+    property Sticky : boolean read fsticky write Fsticky;
+    property EventTime : string read feventtime write Feventtime;
+    property LocalOnly : boolean read flocalonly write flocalonly;
+    property DefaultSound : boolean read Fdefaultsound write Fdefaultsound;
+    property DefaultVibrateTimings : boolean read Fdefaultvibratetimings write Fdefaultvibratetimings;
+    property DefaultLightSettings : boolean read fdefaultlightsettings write Fdefaultlightsettings;
+    property VibrateTimings : TStrings read Fvibratetimings Write Setvibratetimings;
+    property NotificationCount : integer read fnotificationcount write fnotificationcount;
+    property NotificationPriority : TAndroidNotificationPriority Read FNotificationPriority Write FNotificationPriority;
+    property Image : string read fimage write Fimage;
+    property FCMOptions : TFCMOptions Read FFCMoptions Write SetFCMOptions;
+    property Visibility : TAndroidMessageVisibility Read FVisibility Write FVisibility;
+  end;
+
+  { TWebPushConfig }
+
+  { TWebPushFCMOptions }
+
+  TWebPushFCMOptions = class(TFCMOptions)
+  private
+    FLink: string;
+  Public
+    procedure Assign(Source: TPersistent); override;
+    Procedure ToJSON(Obj: TJSONObject); override;
+    function HaveData : Boolean; override;
+    procedure Clear; override;
+
+  Published
+    property link : string read FLink Write FLink;
+  end;
+
+  TWebPushConfig = class(THeadersPlatformConfig)
+  private
+    FData: TStrings;
+    FNotification: TJSONObject;
+    FOPtions: TWebPushFCMOptions;
+    procedure SetData(AValue: TStrings);
+    procedure SetOptions(AValue: TWebPushFCMOptions);
+  Public
+    procedure Assign(Source: TPersistent); override;
+    Constructor Create; override;
+    Destructor destroy; override;
+    procedure Clear; override;
+    procedure ToJSON(Obj : TJSONObject); override;
+    Property notification : TJSONObject Read FNotification;
+  Published
+    property Data : TStrings Read FData Write SetData;
+    property FCMOptions : TWebPushFCMOptions Read FOPtions Write SetOptions;
+  end;
+
+  { TNotificationMessage }
+  TNotificationSendOption = (nsAndroid,nsApple,nsWebPush,nsFCMOptions,nsDataOnly);
+  TNotificationSendOptions = set of TNotificationSendOption;
+  TRecipientType = (rtToken,rtTopic,rtCondition);
+
+  TNotificationMessage = class(TJSONPersist)
+  Public
+    Const DefaultSendOptions = [nsAndroid,nsApple,nsWebPush,nsFCMoptions];
+  private
+    FAndroidConfig: TAndroidConfig;
+    FAppleConfig: TAppleConfig;
+    FBody: string;
+    FData: TStrings;
+    FFCMOptions: TFCMOptions;
+    FImageURL: string;
+    FOptions: TMessageOptions;
+    FRecipient: String;
+    FRecipientType: TRecipientType;
+    FSendOptions: TNotificationSendOptions;
+    FTitle: string;
+    FWebConfig: TWebPushConfig;
+    function IsSendOptionsStored: Boolean;
+    procedure SetAndroidConfig(AValue: TAndroidConfig);
+    procedure SetAppleConfig(AValue: TAppleConfig);
+    procedure SetBody(AValue: string);
+    procedure SetCMOptions(AValue: TFCMOptions);
+    procedure setdata(AValue: TStrings);
+    procedure SetImageURL(AValue: string);
+    procedure SetTitle(AValue: string);
+    procedure SetWebConfig(AValue: TWebPushConfig);
+  Protected
+    function CreateFCMoptions: TFCMOptions; virtual;
+    function CreateAndroidConfig: TAndroidConfig; virtual;
+    function CreateAppleConfig: TAppleConfig; virtual;
+    function CreateWebPushConfig: TWebPushConfig; virtual;
+  public
+    constructor Create;
+    destructor destroy; override;
+
+    procedure ToJSON(aObj : TJSONObject);
+    function Encode : string;
+    procedure Clear;
+    // toplevel properties, valid for all platforms.
+    // Notification data.
+    Property Recipient : String Read FRecipient Write FRecipient;
+    Property RecipientType : TRecipientType Read FRecipientType Write FRecipientType;
+    property Data: TStrings read FData write setdata;
+    property Title: string read FTitle write SetTitle;
+    property Body: string read FBody write SetBody;
+    property Image: string read FImageURL write SetImageURL;
+    // available in Apple and Android, not in web.
+    property Options: TMessageOptions read FOptions write FOptions;
+    Property SendOptions : TNotificationSendOptions Read FSendOptions Write FSendOptions Stored IsSendOptionsStored;
+    // Apple specific
+    property AppleConfig : TAppleConfig Read FAppleConfig Write SetAppleConfig;
+    // Android specific
+    property AndroidConfig : TAndroidConfig Read FAndroidConfig Write SetAndroidConfig;
+    // Web specific
+    Property WebPushConfig : TWebPushConfig Read FWebConfig Write SetWebConfig;
+    // FCM options
+    Property FCMOptions : TFCMOptions Read FFCMOptions Write SetCMOptions;
+  end;
+
+
+
+implementation
+
+uses dateutils, fpfcmstrings;
+
+{ TServiceAccountData }
+
+function TServiceAccountData.Parse(const AJSON: string): Boolean;
+
+var
+  D : TJSONData;
+  O : TJSONObject absolute D;
+
+begin
+  D:=GetJSON(aJSON);
+  try
+    Result:=D is TJSONObject;
+    if Result then
+      begin
+      DoLoadFromJSON(O);
+      Result:=IsValidData;
+      end;
+  finally
+    D.Free;
+  end;
+end;
+
+function TServiceAccountData.IsValidData: Boolean;
+begin
+  Result:=(FAuthURI<>'');
+  Result:=Result and (FClientEmail<>'');
+  Result:=Result and (FClientID<>'');
+  Result:=Result and (FPrivateKey<>'');
+  Result:=Result and (FProjectID<>'');
+  Result:=Result and (FTokenURI<>'');
+end;
+
+{ TGoogleJWT }
+
+function TGoogleJWT.GetGoogleClaims: TGoogleClaims;
+begin
+  Result:=(Claims as TGoogleClaims);
+end;
+
+function TGoogleJWT.CreateClaims: TClaims;
+begin
+  Result:=TGoogleClaims.Create;
+end;
+
+
+{ TJSONPersist }
+
+function TJSONPersist.JSONAdd(Obj: TJSONObject; const aKey, aValue: String): Boolean;
+begin
+  Result:=assigned(Obj) and (aValue<>'') and (aKey<>'');
+  if Result then
+    Obj.Add(aKey,aValue);
+end;
+
+function TJSONPersist.JSONAdd(Obj: TJSONObject; const aKey: String; aValue: TJSONObject): Boolean;
+begin
+  Result:=Assigned(Obj) and (aKey<>'') and Assigned(aValue) and (aValue.Count>0);
+  if Result then
+    Obj.Add(aKey,aValue.Clone);
+end;
+
+function TJSONPersist.JSONAdd(Obj: TJSONObject; const aKey: String; aValue: Integer): Boolean;
+begin
+  Result:=Assigned(Obj) and (aKey<>'') and (aValue<>0);
+  if Result then
+    Obj.Add(aKey,aValue);
+end;
+
+function TJSONPersist.JSONAdd(Obj: TJSONObject; const aKey: String; aValue: Int64): Boolean;
+begin
+  Result:=Assigned(Obj) and (aKey<>'') and (aValue<>0);
+  if Result then
+    Obj.Add(aKey,aValue);
+end;
+
+function TJSONPersist.JSONAdd(Obj: TJSONObject; const aKey: String; aValue: Boolean): Boolean;
+begin
+  Result:=Assigned(Obj) and (aKey<>'') and (aValue);
+  if Result then
+    Obj.Add(aKey,aValue);
+end;
+
+function TJSONPersist.JSONGetSub(Obj: TJSONObject; const aKey: String): TJSONObject;
+
+var
+  Idx : integer;
+
+begin
+  Idx:=Obj.IndexOfName(aKey);
+  if Idx<>-1 then
+    Result:=TJSONObject(Obj.Items[idx])
+  else
+    begin
+    Result:=TJSONObject.Create;
+    Obj.Add(aKey,Result);
+    end;
+end;
+
+function TJSONPersist.JSONAddStrings(Obj: TJSONObject; aValue: TStrings): Integer;
+
+var
+  I : Integer;
+  N,V : String;
+
+begin
+  Result:=0;
+  for I:=0 to aValue.Count-1 do
+    begin
+    aValue.GetNameValue(I,N,V);
+    if (N<>'') then
+      begin
+      Obj.Add(N,V);
+      Inc(Result);
+      end;
+    end;
+end;
+
+function TJSONPersist.JSONAddStringsRaw(Obj: TJSONObject; aKey : String; aValue: TStrings): Integer;
+
+var
+  s : string;
+  arr : TJSONArray;
+
+begin
+  Result:=aValue.Count;
+  if Result=0 then exit;
+  arr:=TJSONArray.Create;
+  obj.Add(aKey,arr);
+  for S in aValue do
+    arr.add(S)
+
+end;
+
+
+{ TPlatformConfig }
+
+procedure TPlatformConfig.AddCustomPayload(aObject: TJSONObject);
+
+var
+  Enum : TJSONEnum;
+
+begin
+  for Enum in CustomPayLoad do
+    if aObject.IndexOfName(Enum.Key)<>-1 then
+      aObject.Add(Enum.Key,Enum.Value.Clone);
+end;
+
+procedure TPlatformConfig.Assign(Source: TPersistent);
+
+var
+  aSource: TPlatformConfig absolute Source;
+
+begin
+  if Source is TPlatformConfig then
+  begin
+    FreeAndNil(FCustomPayLoad);
+    FCustomPayLoad:=(aSource.FCustomPayLoad.Clone as TJSONObject);
+  end else
+    inherited Assign(Source);
+end;
+
+(*
+constructor TPlatformConfig.Create;
+begin
+  FCustomPayLoad:=TJSONObject.Create;
+end;
+
+destructor TPlatformConfig.destroy;
+begin
+  FreeAndNil(FCustomPayLoad);
+  Inherited;
+end;
+*)
+procedure TPlatformConfig.ToJSON(Obj: TJSONObject);
+
+var
+  payload : TJSONObject;
+
+begin
+  if (FCustomPayLoad.Count=0) then
+    exit;
+  PayLoad:=JSONGetSub(Obj,'payload');
+  AddCustomPayLoad(PayLoad);
+end;
+
+{ THeadersPlatformConfig }
+
+procedure THeadersPlatformConfig.SetHeaders(AValue: TStrings);
+
+var
+  I : integer;
+  N,V : String;
+
+begin
+  if FHeaders=AValue then Exit;
+  FHeaders.Clear;
+  For I:=0 to aValue.Count-1 do
+    begin
+    aValue.GetNameValue(I,N,V);
+    if N<>'' then
+      FHeaders.Add(N+': '+V);
+    end;
+end;
+
+
+function THeadersPlatformConfig.addHeaders(Obj: TJSONObject; asSub: Boolean = true): Integer;
+
+var
+  H : TJSONObject;
+
+begin
+  Result:=0;
+  if Headers.Count=0 then exit;
+  if Not AsSub then
+    H:=Obj
+  else
+    h:=JSONGetSub(Obj,'headers');
+  Result:=JSONAddStrings(H,FHeaders);
+end;
+
+procedure THeadersPlatformConfig.Assign(Source: TPersistent);
+var
+  aSource: THeadersPlatformConfig absolute source;
+begin
+  inherited Assign(Source);
+  if Source is THeadersPlatformConfig then
+  begin
+    FHeaders:=aSource.FHeaders;
+  end;
+end;
+
+constructor THeadersPlatformConfig.Create;
+begin
+  inherited Create;
+  FHeaders:=TStringList.Create;
+  TStringList(FHeaders).NameValueSeparator:=':';
+end;
+
+destructor THeadersPlatformConfig.Destroy;
+begin
+  FreeAndNil(FHeaders);
+  inherited destroy;
+end;
+
+procedure THeadersPlatformConfig.Clear;
+begin
+  inherited Clear;
+  FHeaders.Clear;
+end;
+
+{ TAppleConfig }
+
+procedure TAppleConfig.SetFFCMoptions(AValue: TAppleFCMOptions);
+begin
+  if FFCMOPtions=AValue then Exit;
+  FFCMOPtions.Assign(AValue);
+end;
+
+procedure TAppleConfig.AddPayLoad(Obj: TJSONObject);
+
+
+begin
+  if (AlertObject.Count>0) then
+    Obj.Add('alert',AlertObject.Clone)
+  else
+    JSONAdd(Obj,'alert',Alert);
+  JSONAdd(Obj,'badge',Badge);
+  JSONAdd(Obj,'sound',Sound);
+  JSONAdd(Obj,'thread-id',ThreadID);
+  JSONAdd(Obj,'category',Category);
+  JSONAdd(Obj,'mutable-content',MutableContent);
+  JSONAdd(Obj,'target-content-id',TargetContentID);
+  JSONAdd(Obj,'interruption-level',InterruptionLevel);
+  JSONAdd(Obj,'relevance-score',RelevanceScore);
+  JSONAdd(Obj,'filter-criteria',FilterCriteria);
+  JSONAdd(Obj,'stale-date',StaleDate);
+  JSONAdd(Obj,'timestamp',TimeStamp);
+  JSONAdd(Obj,'dismissal-date',DismissalDate);
+  JSONAdd(Obj,'event',Event);
+  JSONAdd(Obj,'attributes-type',AttributesType);
+end;
+
+constructor TAppleConfig.Create;
+begin
+  inherited Create;
+  FAlertObject:=TJSONObject.Create;
+  FFCMoptions:=CreateFCMOptions;
+end;
+
+destructor TAppleConfig.Destroy;
+begin
+  FreeAndNil(FAlertObject);
+  FreeAndNil(FFCMOptions);
+  inherited Destroy;
+end;
+
+
+function TAppleConfig.CreateFCMOptions : TAppleFCMOptions;
+
+begin
+  Result:=TAppleFCMOptions.Create;
+end;
+
+procedure TAppleConfig.Assign(Source: TPersistent);
+var
+  aSource: TAppleConfig absolute Source;
+begin
+  inherited Assign(Source);
+  if Source is TAppleConfig then
+  begin
+    FreeAndNil(FAlertObject);
+    FAlertObject:=(aSource.AlertObject.Clone as TJSONObject);
+    Timestamp:=aSource.Timestamp;
+    ThreadID:=aSource.ThreadID;
+    TargetContentID:=aSource.TargetContentID;
+    StaleDate:=aSource.StaleDate;
+    RelevanceScore:=aSource.RelevanceScore;
+    MutableContent:=aSource.MutableContent;
+    InterruptionLevel:=aSource.InterruptionLevel;
+    Filtercriteria:=aSource.Filtercriteria;
+    FAlertObject:=aSource.FAlertObject;
+    Event:=aSource.Event;
+    DismissalDate:=aSource.DismissalDate;
+    Category:=aSource.Category;
+    Badge:=aSource.Badge;
+    attributestype:=aSource.attributestype;
+    Alert:=aSource.Alert;
+    Sound:=aSource.Sound;
+    FCMOptions:=aSource.FCMOptions;
+  end;
+
+end;
+
+procedure TAppleConfig.ToJSON(Obj: TJSONObject);
+
+var
+  O : TJSONObject;
+
+begin
+  AddHeaders(Obj,true);
+  O:=TJSONObject.Create;
+  try
+    AddPayLoad(O);
+    if CustomPayLoad.Count>0 then
+      AddCustomPayLoad(O);
+  except
+    O.Free;
+    Raise;
+  end;
+  if O.Count=0 then
+    FreeAndNil(O)
+  else
+    Obj.Add('payload',O);
+  if fcmoptions.HaveData then
+    begin
+    O:=TJSONObject.Create;
+    try
+      FCMOptions.ToJSON(O);
+      if O.Count=0 then
+        FreeAndNil(O)
+      else
+        Obj.Add('fcm_options',O);
+    except
+      O.Free;
+      Raise;
+    end;
+    end;
+end;
+
+procedure TAppleConfig.Clear;
+begin
+  inherited Clear;
+  FAlert:='';
+  FAlertObject.Clear;
+  Fattributestype:='';
+  FBadge:=0;
+  fcategory:='';
+  Fdismissaldate:=0;
+  FEvent:='';
+  FFiltercriteria:='';
+  Finterruptionlevel:='';
+  fmutablecontent:=0;
+  Frelevancescore:='';
+  FStaleDate:=0;
+  Ftargetcontentid:='';
+  Fthreadid:='';
+  FTimeStamp:=0;
+  FSound:='';
+  FCMOptions.Clear;
+end;
+
+{ TAndroidConfig }
+
+procedure TAndroidConfig.SetData(AValue: TJSONObject);
+begin
+  if FData=AValue then Exit;
+  if OwnsData then
+    FreeAndNil(FData);
+  FData:=AValue;
+end;
+
+procedure TAndroidConfig.SetFCMOptions(AValue: TFCMOptions);
+begin
+  if FCMoptions=AValue then Exit;
+  FCMoptions.Assign(AValue);
+end;
+
+procedure TAndroidConfig.setbodylocargs(AValue: TStrings);
+begin
+  if fbodylocargs=AValue then Exit;
+  fbodylocargs.Assign(AValue);
+end;
+
+procedure TAndroidConfig.settitlelocargs(AValue: TStrings);
+begin
+  if ftitlelocargs=AValue then Exit;
+  ftitlelocargs.Assign(AValue);
+end;
+
+procedure TAndroidConfig.Setvibratetimings(AValue: TStrings);
+begin
+  if Fvibratetimings=AValue then Exit;
+  Fvibratetimings.Assign(AValue);
+end;
+
+procedure TAndroidConfig.Assign(Source: TPersistent);
+var
+  aSource: TAndroidConfig absolute Source;
+begin
+  inherited Assign(Source);
+  if Source is TAndroidConfig then
+  begin
+    VibrateTimings:=aSource.VibrateTimings;
+    TTL:=aSource.TTL;
+    TitleLocKey:=aSource.TitleLocKey;
+    TitleLocArgs:=aSource.TitleLocArgs;
+    title:=aSource.title;
+    Ticker:=aSource.Ticker;
+    Tag:=aSource.Tag;
+    Sticky:=aSource.Sticky;
+    Sound:=aSource.Sound;
+    RestrictedPackageName:=aSource.RestrictedPackageName;
+    OwnsData:=aSource.OwnsData;
+    NotificationCount:=aSource.NotificationCount;
+    LocalOnly:=aSource.LocalOnly;
+    Image:=aSource.Image;
+    Icon:=aSource.Icon;
+    Fvibratetimings:=aSource.Fvibratetimings;
+    ftitlelocargs:=aSource.ftitlelocargs;
+    Flightsettings:=aSource.Flightsettings;
+    FData:=aSource.FData;
+    fbodylocargs:=aSource.fbodylocargs;
+    EventTime:=aSource.EventTime;
+    DirectBootOk:=aSource.DirectBootOk;
+    DefaultVibrateTimings:=aSource.DefaultVibrateTimings;
+    DefaultSound:=aSource.DefaultSound;
+    DefaultLightSettings:=aSource.DefaultLightSettings;
+    data:=aSource.data;
+    Color:=aSource.Color;
+    CollapseKey:=aSource.CollapseKey;
+    ClickAction:=aSource.ClickAction;
+    ChannelID:=aSource.ChannelID;
+    BodyLocKey:=aSource.BodyLocKey;
+    BodyLocArgs:=aSource.BodyLocArgs;
+    Body:=aSource.Body;
+    FCMoptions:=aSource.FCMoptions;
+    Visibility:=aSource.Visibility;
+    NotificationPriority:=aSource.NotificationPriority;
+  end;
+end;
+
+constructor TAndroidConfig.create;
+begin
+  inherited create;
+  FBodyLocArgs:=TStringList.Create;
+  FTitleLocArgs:=TStringList.Create;
+  FVibrateTimings:=TStringList.Create;
+  FFCMoptions:=TFCMOptions.Create;
+  Flightsettings:=TJSONObject.Create;
+end;
+
+destructor TAndroidConfig.destroy;
+begin
+  If OwnsData then
+    FreeAndNil(FData);
+  FreeAndNil(Flightsettings);
+  FreeAndNil(FBodyLocArgs);
+  FreeAndNil(FTitleLocArgs);
+  FreeAndNil(FVibrateTimings);
+  FreeAndNil(FFCMoptions);
+  inherited destroy;
+end;
+
+procedure TAndroidConfig.Clear;
+begin
+  inherited Clear;
+  Fbody:='';
+  fbodylocargs.Clear;
+  fbodylockey:='';
+  FChannelID:='';
+  FClickAction:='';
+  FCollapseKey:='';
+  fcolor:='';
+  if OwnsData then
+    FreeAndNil(FData)
+  else
+    FData:=Nil;
+  fdefaultlightsettings:=False;
+  Fdefaultsound:=False;
+  Fdefaultvibratetimings:=False;
+  fdirectbootOK:=False;
+  Feventtime:='';
+  Ficon:='';
+  fimage:='';
+  Flightsettings.Clear;
+  flocalonly:=False;
+  fnotificationcount:=0;
+  FOwnsData:=False;
+  Frestrictedpackagename:='';
+  fsound:='';
+  fsticky:=False;
+  ftag:='';
+  fticker:='';
+  Ftitle:='';
+  ftitlelocargs.Clear;
+  ftitlelockey:='';
+  Fttl:='';
+  Fvibratetimings.Clear;
+  FFCMoptions.Clear;
+  Visibility:=mvUnspecified;
+  FNotificationPriority:=npUnspecified;
+end;
+
+procedure TAndroidConfig.ToJSON(Obj: TJSONObject);
+
+Const
+  MsgPrio : Array[TAndroidMessagePriority] of string = ('','normal','high');
+  MsgVis : Array[TAndroidMessageVisibility] of integer = (0,0,1,-1);
+  NotifPrio : Array[TAndroidNotificationPriority] of string = ('UNSPECIFIED','MIN','LOW','DEFAULT','HIGH','MAX');
+var
+  O : TJSONObject;
+
+begin
+  JSONAdd(Obj,'collapse_key',CollapseKey);
+  JSONAdd(Obj,'priority',MsgPrio[priority]);
+  JSONAdd(Obj,'ttl',ttl);
+  JSONAdd(Obj,'restricted_package_name',RestrictedPackageName);
+  if Assigned(Data) and (Data.Count>0) then
+    obj.Add('data',Data.Clone);
+  O:=JSONGetSub(Obj,'notification');
+  JSONAdd(O,'title',Title);
+  JSONAdd(O,'body',Body);
+  JSONAdd(O,'icon',Icon);
+  JSONAdd(O,'color',Color);
+  JSONAdd(O,'sound',sound);
+  JSONAdd(O,'tag',tag);
+  JSONAdd(O,'click_action',ClickAction);
+  JSONAdd(O,'body_loc_key',bodylockey);
+  JSONAddStringsRaw(O,'body_loc_args',BodyLocArgs);
+  JSONAdd(O,'title_loc_key',titlelockey);
+  JSONAddStringsRaw(O,'title_loc_args',TitleLocArgs);
+  JSONAdd(O,'channel_id',ChannelID);
+  JSONAdd(O,'ticker',Ticker);
+  JSONAdd(O,'sticky',Sticky);
+  JSONAdd(O,'event_time',EventTime);
+  JSONAdd(O,'local_only',LocalOnly);
+  JSONAdd(O,'default_sound',defaultsound);
+  JSONAdd(O,'default_vibrate_timings',DefaultVibrateTimings);
+  JSONAdd(O,'default_light_settings',DefaultLightSettings);
+  JSONAddStringsRaw(O,'vibrate_timings',VibrateTimings);
+  if Visibility<>mvUnspecified then
+    JSONAdd(O,'visibility',MsgVis[Visibility]);
+  if NotificationPriority<>npUnspecified then
+    JSONAdd(O,'notification_priority','PRIORITY_'+NotifPrio[NotificationPriority]);
+  JSONAdd(O,'notification_count',NotificationCount);
+  if lightsettings.Count>0 then
+    O.Add('light_settings',lightsettings.Clone);
+  JSONAdd(O,'image',image);
+  JSONAdd(Obj,'direct_boot_ok',DirectBootOk);
+  if FCMOptions.HaveData then
+    begin
+    O:=JSONGetSub(Obj,'fcm_options');
+    FCMOptions.ToJSON(O);
+    end;
+end;
+
+{ TWebPushFCMOptions }
+
+procedure TWebPushFCMOptions.Assign(Source: TPersistent);
+var
+  aSource: TWebPushFCMOptions absolute source;
+begin
+  inherited Assign(Source);
+  if Source is TWebPushFCMOptions then
+  begin
+    link:=aSource.link;
+  end;
+end;
+
+procedure TWebPushFCMOptions.ToJSON(Obj: TJSONObject);
+begin
+  inherited ToJSON(Obj);
+  JSONAdd(obj,'link',Link);
+end;
+
+function TWebPushFCMOptions.HaveData: Boolean;
+begin
+  Result:=inherited HaveData;
+  Result:=Result and (link<>'');
+end;
+
+procedure TWebPushFCMOptions.Clear;
+begin
+  inherited Clear;
+  link:='';
+end;
+
+{ TWebPushConfig }
+
+procedure TWebPushConfig.SetData(AValue: TStrings);
+begin
+  if FData=AValue then Exit;
+  FData.Assign(AValue);
+end;
+
+procedure TWebPushConfig.SetOptions(AValue: TWebPushFCMOptions);
+begin
+  if FOPtions=AValue then Exit;
+  FOptions.Assign(AValue);
+end;
+
+procedure TWebPushConfig.Assign(Source: TPersistent);
+
+var
+  aSource: TWebPushConfig absolute source;
+
+begin
+  inherited Assign(Source);
+  if Source is TWebPushConfig then
+  begin
+    FreeAndNil(FNotification);
+    FNotification:=aSource.FNotification.Clone as TJSONObject;
+    Data:=aSource.Data;
+    FCMOptions:=aSource.FCMOptions;
+  end;
+end;
+
+constructor TWebPushConfig.Create;
+begin
+  inherited Create;
+  FData:=TStringList.Create;
+  FData.NameValueSeparator:=':';
+  FNotification:=TJSONObject.Create;
+  FOptions:=TWebPushFCMOptions.Create;
+end;
+
+destructor TWebPushConfig.destroy;
+begin
+  FreeAndNil(FOptions);
+  FreeAndNil(FHeaders);
+  FreeAndNil(FData);
+  FreeAndNil(FNotification);
+  inherited destroy;
+end;
+
+procedure TWebPushConfig.Clear;
+begin
+  inherited Clear;
+  Data.Clear;
+  Notification.Clear;
+  FCMOptions.Clear;
+end;
+
+procedure TWebPushConfig.ToJSON(Obj: TJSONObject);
+
+var
+  O : TJSONObject;
+
+begin
+  inherited ToJSON(Obj);
+  AddHeaders(Obj,true);
+  if FData.Count>0 then
+    begin
+    O:=JSONGetSub(obj,'data');
+    JSONAddStrings(O,FData);
+    end;
+  if Notification.Count>0 then
+    Obj.Add('notification',Notification.clone);
+  if FCMOptions.HaveData then
+    begin
+    O:=JSONGetSub(obj,'fcm_options');
+    FCMOptions.ToJSON(O);
+    end;
+end;
+
+{ TFCMOptions }
+
+procedure TFCMOptions.Assign(Source: TPersistent);
+var
+  aSource: TFCMOptions absolute Source;
+begin
+  inherited Assign(Source);
+  if Source is TFCMOptions then
+  begin
+    AnalyticsLabel:=aSource.AnalyticsLabel;
+  end;
+end;
+
+procedure TFCMOptions.ToJSON(Obj: TJSONObject);
+begin
+  JSONAdd(Obj,'analytics_label',AnalyticsLabel)
+end;
+
+function TFCMOptions.HaveData: Boolean;
+begin
+  Result:=(AnalyticsLabel<>'');
+end;
+
+procedure TFCMOptions.Clear;
+begin
+  inherited Clear;
+  AnalyticsLabel:='';;
+end;
+
+{ TAppleFCMOptions }
+
+procedure TAppleFCMOptions.Assign(Source: TPersistent);
+
+var
+  aSource : TAppleFCMOptions absolute Source;
+
+begin
+  inherited Assign(Source);
+  if Source is TAppleFCMOptions then
+    Image:=aSource.Image;
+end;
+
+procedure TAppleFCMOptions.ToJSON(Obj: TJSONObject);
+begin
+  inherited ToJSON(Obj);
+  JSONAdd(Obj,'image',image)
+end;
+
+function TAppleFCMOptions.HaveData: Boolean;
+begin
+  Result:=inherited HaveData;
+  Result:=Result or (Image<>'')
+end;
+
+procedure TAppleFCMOptions.Clear;
+begin
+  inherited Clear;
+  Image:='';
+end;
+
+{ TPlatformOptions }
+
+constructor TPlatformConfig.Create;
+begin
+  FCustomPayLoad:=TJSONObject.Create;
+end;
+
+destructor TPlatformConfig.Destroy;
+begin
+  FreeAndNil(FCustomPayLoad);
+  Inherited;
+end;
+
+procedure TPlatformConfig.Clear;
+begin
+  FCustomPayLoad.Clear;
+end;
+
+{ TNotificationMessage }
+
+
+function TNotificationMessage.IsSendOptionsStored: Boolean;
+begin
+  Result:=FSendOptions<>DefaultSendOptions;
+end;
+
+procedure TNotificationMessage.SetAndroidConfig(AValue: TAndroidConfig);
+begin
+  if FAndroidConfig=AValue then Exit;
+  FAndroidConfig:=AValue;
+end;
+
+procedure TNotificationMessage.SetAppleConfig(AValue: TAppleConfig);
+begin
+  if FAppleConfig=AValue then Exit;
+  FAppleConfig:=AValue;
+end;
+
+procedure TNotificationMessage.SetBody(AValue: string);
+begin
+  if FBody=AValue then Exit;
+  FBody:=AValue;
+end;
+
+procedure TNotificationMessage.SetCMOptions(AValue: TFCMOptions);
+begin
+  if FFCMOptions=AValue then Exit;
+  FFCMOptions.Assign(AValue);
+end;
+
+procedure TNotificationMessage.setdata(AValue: TStrings);
+begin
+  if FData=AValue then Exit;
+  FData.Assign(aValue);
+end;
+
+procedure TNotificationMessage.SetImageURL(AValue: string);
+begin
+  if FImageURL=AValue then Exit;
+  FImageURL:=AValue;
+end;
+
+procedure TNotificationMessage.SetTitle(AValue: string);
+begin
+  if FTitle=AValue then Exit;
+  FTitle:=AValue;
+end;
+
+procedure TNotificationMessage.SetWebConfig(AValue: TWebPushConfig);
+begin
+  if FWebConfig=AValue then Exit;
+  FWebConfig.Assign(aValue);
+end;
+
+function TNotificationMessage.CreateAppleConfig : TAppleConfig;
+
+begin
+  Result:=TAppleConfig.Create;
+end;
+
+function TNotificationMessage.CreateWebPushConfig : TWebPushConfig;
+
+begin
+  Result:=TWebPushConfig.Create;
+end;
+
+function TNotificationMessage.CreateFCMoptions: TFCMOptions;
+
+begin
+  Result:=TFCMOptions.Create;
+end;
+
+procedure TNotificationMessage.ToJSON(aObj: TJSONObject);
+
+const
+  RecipientNames : Array[TRecipientType] of string
+                 = ('token','topic','condition');
+
+var
+  O : TJSONObject;
+
+begin
+  if Data.Count>0 then
+    begin
+    O:=JSONGetSub(aObj,'data');
+    JSONAddStrings(o,Data);
+    end;
+  if not (nsDataOnly in SendOptions) then
+    begin
+    O:=JSONGetSub(aObj,'notification');
+    JSONAdd(O,'title',Title);
+    JSONAdd(O,'body',body);
+    JSONAdd(O,'image',Image);
+    end;
+  if nsAndroid in SendOptions then
+    begin
+    O:=JSONGetSub(aObj,'android');
+    AndroidConfig.ToJSON(O);
+    end;
+  if nsWebPush in SendOptions then
+    begin
+    O:=JSONGetSub(aObj,'webpush');
+    WebPushConfig.ToJSON(O);
+    end;
+  if nsApple in SendOptions then
+    begin
+    O:=JSONGetSub(aObj,'apns');
+    AppleConfig.ToJSON(O);
+    end;
+  if nsFCMOptions in SendOptions then
+    begin
+    O:=JSONGetSub(aObj,'fcm_options');
+    FCMOptions.ToJSON(O);
+    end;
+  JSONAdd(aObj,RecipientNames[RecipientType],Recipient);
+end;
+
+
+function TNotificationMessage.CreateAndroidConfig : TAndroidConfig;
+
+begin
+  Result:=TAndroidConfig.Create;
+end;
+
+constructor TNotificationMessage.Create;
+begin
+  Inherited;
+  FSendOptions:=DefaultSendOptions;
+  FAppleConfig:=CreateAppleConfig;
+  FAndroidConfig:=CreateAndroidConfig;
+  FWebConfig:=CreateWebPushConfig;
+  FFCMOptions:=CreateFCMoptions;
+  FData:=TStringList.Create;
+end;
+
+destructor TNotificationMessage.destroy;
+begin
+  FreeAndNil(FFCMOptions);
+  FreeAndNil(FData);
+  FreeAndNil(FAppleConfig);
+  FreeAndNil(FAndroidConfig);
+  FreeAndNil(FWebConfig);
+  inherited destroy;
+end;
+
+function TNotificationMessage.Encode: string;
+
+var
+  Obj : TJSONObject;
+
+begin
+  Obj:=TJSONObject.Create;
+  try
+    ToJSON(Obj);
+    Result:=Obj.AsJSON;
+  finally
+    Obj.Free;
+  end;
+end;
+
+procedure TNotificationMessage.Clear;
+begin
+  Recipient:='';
+  Title:='';
+  Body:='';
+  Image:='';
+  FData.Clear;
+  AppleConfig.Clear;
+  AndroidConfig.Clear;
+  WebPushConfig.Clear;
+end;
+
+{ TBearerToken }
+
+function TBearerToken.GetTokenDateTime: string;
+begin
+  result:=DateToISO8601(FTokenDateTime,False);
+end;
+
+procedure TBearerToken.SetTokenDateTime(AValue: string);
+begin
+  if not TryISO8601ToDate(aValue,FTokenDateTime,False) then
+    FTokenDateTime:=0;
+end;
+
+procedure TBearerToken.SaveToStream(aStream: TStream);
+
+var
+  aJSON : TJSONStringType;
+
+begin
+  aJSON:=AsString;
+  aStream.WriteBuffer(aJSON[1],Length(aJSON)*SizeOf(TJSONCharType));
+end;
+
+procedure TBearerToken.SaveToFile(const aFileName: String);
+
+var
+  F : TFileStream;
+
+begin
+  F:=TFileStream.Create(aFileName,fmCreate);
+  try
+    SaveToStream(F);
+  finally
+    F.Free;
+  end;
+end;
+
+procedure TBearerToken.LoadFromStream(aStream: TStream);
+
+var
+  D : TJSONData;
+  O : TJSONObject absolute D;
+
+begin
+  D:=GetJSON(aStream);
+  try
+    if D.JSONType<>jtObject then
+      Raise EFCM.Create('Stream does not contain a valid JSON object');
+    LoadFromJSON(O);
+  finally
+    D.Free;
+  end;
+end;
+
+procedure TBearerToken.LoadFromFile(const aFileName: String);
+var
+  F : TFileStream;
+
+begin
+  F:=TFileStream.Create(aFileName,fmOpenRead or fmShareDenyWrite);
+  try
+    LoadFromStream(F);
+  finally
+    F.Free;
+  end;
+end;
+
+function TBearerToken.IsValidData: Boolean;
+
+begin
+  Result:=(faccess_token<>'') or (frefresh_token<>'');
+  Result:=Result and (FExpires_in<>0);
+  Result:=Result and (Fid_token<>'');
+  Result:=Result and (fscope<>'');
+  Result:=Result and (ftoken_type<>'');
+end;
+
+function TBearerToken.IsExpired: Boolean;
+begin
+  Result:=(access_token='');
+  Result:=Result or (SecondsBetween(Now,TokenDateTime) > expires_in);
+end;
+
+(*
+function TBearerToken.Parse(const AJSON: string): Boolean;
+var
+  D : TJSONData;
+  O : TJSONObject absolute D;
+
+begin
+  D:=GetJSON(aJSON);
+  try
+    Result:=D is TJSONObject;
+    if Result then
+      begin
+      DoLoadFromJSON(O);
+      Result:=IsValidData;
+      end;
+  finally
+    D.Free;
+  end;
+end;
+*)
+end.
+