├── Bin ├── libeay32.dll ├── nghttp2.dll └── ssleay32.dll ├── Example.Windows ├── DelphiRemotePushSender.dpr ├── DelphiRemotePushSender.dproj ├── DelphiRemotePushSender.res ├── FMain.dfm └── FMain.pas ├── License.txt ├── Nghttp2.pas ├── README.md └── ReceivingPushNotifications.md /Bin/libeay32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grijjy/DelphiRemotePushSender/f85c96b4efb8d902658d441d80fbde45d14c7721/Bin/libeay32.dll -------------------------------------------------------------------------------- /Bin/nghttp2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grijjy/DelphiRemotePushSender/f85c96b4efb8d902658d441d80fbde45d14c7721/Bin/nghttp2.dll -------------------------------------------------------------------------------- /Bin/ssleay32.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grijjy/DelphiRemotePushSender/f85c96b4efb8d902658d441d80fbde45d14c7721/Bin/ssleay32.dll -------------------------------------------------------------------------------- /Example.Windows/DelphiRemotePushSender.dpr: -------------------------------------------------------------------------------- 1 | program DelphiRemotePushSender; 2 | 3 | uses 4 | Vcl.Forms, 5 | FMain in 'FMain.pas' {FormMain}, 6 | Grijjy.RemotePush.Sender in '..\..\GrijjyFoundation\Grijjy.RemotePush.Sender.pas', 7 | Grijjy.Bson in '..\..\GrijjyFoundation\Grijjy.Bson.pas', 8 | Grijjy.Http in '..\..\GrijjyFoundation\Grijjy.Http.pas', 9 | Grijjy.SocketPool.Win in '..\..\GrijjyFoundation\Grijjy.SocketPool.Win.pas', 10 | Grijjy.Uri in '..\..\GrijjyFoundation\Grijjy.Uri.pas', 11 | Grijjy.Winsock2 in '..\..\GrijjyFoundation\Grijjy.Winsock2.pas', 12 | Grijjy.OpenSSL in '..\..\GrijjyFoundation\Grijjy.OpenSSL.pas', 13 | Grijjy.OpenSSL.API in '..\..\GrijjyFoundation\Grijjy.OpenSSL.API.pas', 14 | Grijjy.MemoryPool in '..\..\GrijjyFoundation\Grijjy.MemoryPool.pas', 15 | Grijjy.Collections in '..\..\GrijjyFoundation\Grijjy.Collections.pas', 16 | Grijjy.BinaryCoding in '..\..\GrijjyFoundation\Grijjy.BinaryCoding.pas', 17 | Nghttp2 in '..\Nghttp2.pas', 18 | Grijjy.SysUtils in '..\..\GrijjyFoundation\Grijjy.SysUtils.pas', 19 | Grijjy.DateUtils in '..\..\GrijjyFoundation\Grijjy.DateUtils.pas', 20 | Grijjy.Bson.IO in '..\..\GrijjyFoundation\Grijjy.Bson.IO.pas'; 21 | 22 | {$R *.res} 23 | 24 | begin 25 | Application.Initialize; 26 | Application.MainFormOnTaskbar := True; 27 | Application.CreateForm(TFormMain, FormMain); 28 | Application.Run; 29 | end. 30 | -------------------------------------------------------------------------------- /Example.Windows/DelphiRemotePushSender.dproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | {DC769454-4F24-42BF-9554-DA0A49F5D879} 4 | 18.2 5 | VCL 6 | DelphiRemotePushSender.dpr 7 | True 8 | Debug 9 | Win32 10 | 1 11 | Application 12 | 13 | 14 | true 15 | 16 | 17 | true 18 | Base 19 | true 20 | 21 | 22 | true 23 | Base 24 | true 25 | 26 | 27 | true 28 | Base 29 | true 30 | 31 | 32 | true 33 | Cfg_1 34 | true 35 | true 36 | 37 | 38 | true 39 | Base 40 | true 41 | 42 | 43 | true 44 | Cfg_2 45 | true 46 | true 47 | 48 | 49 | DelphiRemotePushSender 50 | $(BDS)\bin\delphi_PROJECTICON.ico 51 | $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png 52 | System;Xml;Data;Datasnap;Web;Soap;Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;$(DCC_Namespace) 53 | $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png 54 | CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= 55 | 1033 56 | .\$(Platform)\$(Config) 57 | ..\Bin 58 | false 59 | false 60 | false 61 | false 62 | false 63 | 64 | 65 | Debug 66 | true 67 | DBXSqliteDriver;RESTComponents;fmxase;DBXDb2Driver;DBXInterBaseDriver;vclactnband;vclFireDAC;emsclientfiredac;tethering;svnui;DataSnapFireDAC;FireDACADSDriver;DBXMSSQLDriver;DatasnapConnectorsFreePascal;FireDACMSSQLDriver;vcltouch;vcldb;bindcompfmx;svn;DBXOracleDriver;inetdb;emsedge;FireDACIBDriver;fmx;fmxdae;FireDACDBXDriver;dbexpress;IndyCore;vclx;dsnap;emsclient;DataSnapCommon;FireDACCommon;RESTBackendComponents;DataSnapConnectors;VCLRESTComponents;soapserver;vclie;bindengine;DBXMySQLDriver;CloudService;FireDACOracleDriver;FireDACMySQLDriver;DBXFirebirdDriver;FireDACCommonODBC;FireDACCommonDriver;DataSnapClient;inet;IndyIPCommon;bindcompdbx;vcl;IndyIPServer;DBXSybaseASEDriver;IndySystem;FireDACDb2Driver;dsnapcon;FireDACMSAccDriver;fmxFireDAC;FireDACInfxDriver;vclimg;FireDAC;emshosting;FireDACSqliteDriver;FireDACPgDriver;FireDACASADriver;DBXOdbcDriver;FireDACTDataDriver;soaprtl;DbxCommonDriver;DataSnapServer;xmlrtl;soapmidas;DataSnapNativeClient;fmxobj;vclwinx;FireDACDSDriver;rtl;DbxClientDriver;DBXSybaseASADriver;CustomIPTransport;vcldsnap;bindcomp;appanalytics;DBXInformixDriver;IndyIPClient;bindcompvcl;dbxcds;VclSmp;adortl;FireDACODBCDriver;DataSnapIndy10ServerTransport;dsnapxml;DataSnapProviderClient;dbrtl;IndyProtocols;inetdbxpress;FireDACMongoDBDriver;DataSnapServerMidas;$(DCC_UsePackage) 68 | Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) 69 | $(BDS)\bin\default_app.manifest 70 | CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= 71 | 1033 72 | 73 | 74 | DBXSqliteDriver;RESTComponents;fmxase;DBXDb2Driver;DBXInterBaseDriver;vclactnband;vclFireDAC;emsclientfiredac;tethering;DataSnapFireDAC;FireDACADSDriver;DBXMSSQLDriver;DatasnapConnectorsFreePascal;FireDACMSSQLDriver;vcltouch;vcldb;bindcompfmx;DBXOracleDriver;inetdb;emsedge;FireDACIBDriver;fmx;fmxdae;FireDACDBXDriver;dbexpress;IndyCore;vclx;dsnap;emsclient;DataSnapCommon;FireDACCommon;RESTBackendComponents;DataSnapConnectors;VCLRESTComponents;soapserver;vclie;bindengine;DBXMySQLDriver;CloudService;FireDACOracleDriver;FireDACMySQLDriver;DBXFirebirdDriver;FireDACCommonODBC;FireDACCommonDriver;DataSnapClient;inet;IndyIPCommon;bindcompdbx;vcl;IndyIPServer;DBXSybaseASEDriver;IndySystem;FireDACDb2Driver;dsnapcon;FireDACMSAccDriver;fmxFireDAC;FireDACInfxDriver;vclimg;FireDAC;emshosting;FireDACSqliteDriver;FireDACPgDriver;FireDACASADriver;DBXOdbcDriver;FireDACTDataDriver;soaprtl;DbxCommonDriver;DataSnapServer;xmlrtl;soapmidas;DataSnapNativeClient;fmxobj;vclwinx;FireDACDSDriver;rtl;DbxClientDriver;DBXSybaseASADriver;CustomIPTransport;vcldsnap;bindcomp;appanalytics;DBXInformixDriver;IndyIPClient;bindcompvcl;dbxcds;VclSmp;adortl;FireDACODBCDriver;DataSnapIndy10ServerTransport;dsnapxml;DataSnapProviderClient;dbrtl;IndyProtocols;inetdbxpress;FireDACMongoDBDriver;DataSnapServerMidas;$(DCC_UsePackage) 75 | 76 | 77 | DEBUG;$(DCC_Define) 78 | true 79 | false 80 | true 81 | true 82 | true 83 | 84 | 85 | true 86 | true 87 | 1033 88 | true 89 | false 90 | 91 | 92 | false 93 | RELEASE;$(DCC_Define) 94 | 0 95 | 0 96 | 97 | 98 | true 99 | true 100 | 101 | 102 | 103 | MainSource 104 | 105 | 106 |
FormMain
107 | dfm 108 |
109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | Cfg_2 126 | Base 127 | 128 | 129 | Base 130 | 131 | 132 | Cfg_1 133 | Base 134 | 135 |
136 | 137 | Delphi.Personality.12 138 | Application 139 | 140 | 141 | 142 | DelphiRemotePushSender.dpr 143 | 144 | 145 | Microsoft Office 2000 Sample Automation Server Wrapper Components 146 | Microsoft Office XP Sample Automation Server Wrapper Components 147 | 148 | 149 | 150 | 151 | 152 | DelphiRemotePushSender.exe 153 | true 154 | 155 | 156 | 157 | 158 | 1 159 | 160 | 161 | Contents\MacOS 162 | 1 163 | 164 | 165 | Contents\MacOS 166 | 0 167 | 168 | 169 | 170 | 171 | classes 172 | 1 173 | 174 | 175 | 176 | 177 | library\lib\armeabi-v7a 178 | 1 179 | 180 | 181 | 182 | 183 | library\lib\armeabi 184 | 1 185 | 186 | 187 | 188 | 189 | library\lib\mips 190 | 1 191 | 192 | 193 | 194 | 195 | library\lib\armeabi-v7a 196 | 1 197 | 198 | 199 | 200 | 201 | res\drawable 202 | 1 203 | 204 | 205 | 206 | 207 | res\values 208 | 1 209 | 210 | 211 | 212 | 213 | res\drawable 214 | 1 215 | 216 | 217 | 218 | 219 | res\drawable-xxhdpi 220 | 1 221 | 222 | 223 | 224 | 225 | res\drawable-ldpi 226 | 1 227 | 228 | 229 | 230 | 231 | res\drawable-mdpi 232 | 1 233 | 234 | 235 | 236 | 237 | res\drawable-hdpi 238 | 1 239 | 240 | 241 | 242 | 243 | res\drawable-xhdpi 244 | 1 245 | 246 | 247 | 248 | 249 | res\drawable-small 250 | 1 251 | 252 | 253 | 254 | 255 | res\drawable-normal 256 | 1 257 | 258 | 259 | 260 | 261 | res\drawable-large 262 | 1 263 | 264 | 265 | 266 | 267 | res\drawable-xlarge 268 | 1 269 | 270 | 271 | 272 | 273 | 1 274 | 275 | 276 | Contents\MacOS 277 | 1 278 | 279 | 280 | 0 281 | 282 | 283 | 284 | 285 | Contents\MacOS 286 | 1 287 | .framework 288 | 289 | 290 | 0 291 | 292 | 293 | 294 | 295 | 1 296 | .dylib 297 | 298 | 299 | 1 300 | .dylib 301 | 302 | 303 | 1 304 | .dylib 305 | 306 | 307 | Contents\MacOS 308 | 1 309 | .dylib 310 | 311 | 312 | 0 313 | .dll;.bpl 314 | 315 | 316 | 317 | 318 | 1 319 | .dylib 320 | 321 | 322 | 1 323 | .dylib 324 | 325 | 326 | 1 327 | .dylib 328 | 329 | 330 | Contents\MacOS 331 | 1 332 | .dylib 333 | 334 | 335 | 0 336 | .bpl 337 | 338 | 339 | 340 | 341 | 0 342 | 343 | 344 | 0 345 | 346 | 347 | 0 348 | 349 | 350 | 0 351 | 352 | 353 | Contents\Resources\StartUp\ 354 | 0 355 | 356 | 357 | 0 358 | 359 | 360 | 361 | 362 | 1 363 | 364 | 365 | 1 366 | 367 | 368 | 1 369 | 370 | 371 | 372 | 373 | 1 374 | 375 | 376 | 1 377 | 378 | 379 | 1 380 | 381 | 382 | 383 | 384 | 1 385 | 386 | 387 | 1 388 | 389 | 390 | 1 391 | 392 | 393 | 394 | 395 | 1 396 | 397 | 398 | 1 399 | 400 | 401 | 1 402 | 403 | 404 | 405 | 406 | 1 407 | 408 | 409 | 1 410 | 411 | 412 | 1 413 | 414 | 415 | 416 | 417 | 1 418 | 419 | 420 | 1 421 | 422 | 423 | 1 424 | 425 | 426 | 427 | 428 | 1 429 | 430 | 431 | 1 432 | 433 | 434 | 1 435 | 436 | 437 | 438 | 439 | 1 440 | 441 | 442 | 443 | 444 | ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF 445 | 1 446 | 447 | 448 | ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF 449 | 1 450 | 451 | 452 | 453 | 454 | 1 455 | 456 | 457 | 1 458 | 459 | 460 | 461 | 462 | ..\ 463 | 1 464 | 465 | 466 | ..\ 467 | 1 468 | 469 | 470 | 471 | 472 | 1 473 | 474 | 475 | 1 476 | 477 | 478 | 1 479 | 480 | 481 | 482 | 483 | 1 484 | 485 | 486 | 1 487 | 488 | 489 | 1 490 | 491 | 492 | 493 | 494 | ..\ 495 | 1 496 | 497 | 498 | 499 | 500 | Contents 501 | 1 502 | 503 | 504 | 505 | 506 | Contents\Resources 507 | 1 508 | 509 | 510 | 511 | 512 | library\lib\armeabi-v7a 513 | 1 514 | 515 | 516 | 1 517 | 518 | 519 | 1 520 | 521 | 522 | 1 523 | 524 | 525 | 1 526 | 527 | 528 | Contents\MacOS 529 | 1 530 | 531 | 532 | 0 533 | 534 | 535 | 536 | 537 | 1 538 | 539 | 540 | 1 541 | 542 | 543 | 544 | 545 | Assets 546 | 1 547 | 548 | 549 | Assets 550 | 1 551 | 552 | 553 | 554 | 555 | Assets 556 | 1 557 | 558 | 559 | Assets 560 | 1 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | True 574 | False 575 | 576 | 577 | 12 578 | 579 | 580 | 581 | 582 |
583 | -------------------------------------------------------------------------------- /Example.Windows/DelphiRemotePushSender.res: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grijjy/DelphiRemotePushSender/f85c96b4efb8d902658d441d80fbde45d14c7721/Example.Windows/DelphiRemotePushSender.res -------------------------------------------------------------------------------- /Example.Windows/FMain.dfm: -------------------------------------------------------------------------------- 1 | object FormMain: TFormMain 2 | Left = 0 3 | Top = 0 4 | Caption = 'FormMain' 5 | ClientHeight = 339 6 | ClientWidth = 837 7 | Color = clBtnFace 8 | Font.Charset = DEFAULT_CHARSET 9 | Font.Color = clWindowText 10 | Font.Height = -11 11 | Font.Name = 'Tahoma' 12 | Font.Style = [] 13 | OldCreateOrder = False 14 | OnShow = FormShow 15 | PixelsPerInch = 96 16 | TextHeight = 13 17 | object LabelDeviceToken: TLabel 18 | Left = 416 19 | Top = 19 20 | Width = 64 21 | Height = 13 22 | Caption = 'Device Token' 23 | end 24 | object LabelTitle: TLabel 25 | Left = 416 26 | Top = 75 27 | Width = 20 28 | Height = 13 29 | Caption = 'Title' 30 | end 31 | object LabelMessage: TLabel 32 | Left = 416 33 | Top = 131 34 | Width = 42 35 | Height = 13 36 | Caption = 'Message' 37 | end 38 | object ButtonSend: TButton 39 | Left = 750 40 | Top = 296 41 | Width = 75 42 | Height = 25 43 | Caption = 'Send' 44 | TabOrder = 4 45 | OnClick = ButtonSendClick 46 | end 47 | object GroupBoxPlatform: TGroupBox 48 | Left = 8 49 | Top = 8 50 | Width = 377 51 | Height = 305 52 | Caption = 'Platform' 53 | TabOrder = 0 54 | object LabelAndroidAPIKey: TLabel 55 | Left = 24 56 | Top = 224 57 | Width = 75 58 | Height = 13 59 | Caption = 'Android APIKey' 60 | end 61 | object LabelAPNSTopic: TLabel 62 | Left = 24 63 | Top = 48 64 | Width = 54 65 | Height = 13 66 | Caption = 'APNS Topic' 67 | end 68 | object LabelAPNSCertificate: TLabel 69 | Left = 24 70 | Top = 94 71 | Width = 79 72 | Height = 13 73 | Caption = 'APNS Certificate' 74 | end 75 | object LabelAPNSKey: TLabel 76 | Left = 24 77 | Top = 141 78 | Width = 47 79 | Height = 13 80 | Caption = 'APNS Key' 81 | end 82 | object RadioButtoniOS: TRadioButton 83 | Left = 8 84 | Top = 24 85 | Width = 113 86 | Height = 17 87 | Caption = 'iOS' 88 | Checked = True 89 | TabOrder = 0 90 | TabStop = True 91 | OnClick = RadioButtoniOSClick 92 | end 93 | object RadioButtonAndroid: TRadioButton 94 | Left = 8 95 | Top = 199 96 | Width = 113 97 | Height = 17 98 | Caption = 'Android' 99 | TabOrder = 7 100 | OnClick = RadioButtonAndroidClick 101 | end 102 | object EditAndroidAPIKey: TEdit 103 | Left = 24 104 | Top = 243 105 | Width = 322 106 | Height = 21 107 | TabOrder = 6 108 | end 109 | object EditAPNSTopic: TEdit 110 | Left = 24 111 | Top = 67 112 | Width = 322 113 | Height = 21 114 | TabOrder = 1 115 | Text = 'com.mycool.app' 116 | end 117 | object EditAPNSCertificate: TEdit 118 | Left = 24 119 | Top = 112 120 | Width = 233 121 | Height = 21 122 | TabOrder = 2 123 | end 124 | object EditAPNSKey: TEdit 125 | Left = 24 126 | Top = 160 127 | Width = 233 128 | Height = 21 129 | TabOrder = 4 130 | end 131 | object ButtonBrowseCertificate: TButton 132 | Left = 271 133 | Top = 110 134 | Width = 75 135 | Height = 25 136 | Caption = 'Browse Cert' 137 | TabOrder = 3 138 | OnClick = ButtonBrowseCertificateClick 139 | end 140 | object ButtonBrowseKey: TButton 141 | Left = 271 142 | Top = 158 143 | Width = 75 144 | Height = 25 145 | Caption = 'Browse Key' 146 | TabOrder = 5 147 | OnClick = ButtonBrowseKeyClick 148 | end 149 | end 150 | object EditDeviceToken: TEdit 151 | Left = 416 152 | Top = 38 153 | Width = 409 154 | Height = 21 155 | TabOrder = 1 156 | end 157 | object EditTitle: TEdit 158 | Left = 416 159 | Top = 94 160 | Width = 409 161 | Height = 21 162 | TabOrder = 2 163 | Text = 'My Title' 164 | end 165 | object MemoMessage: TMemo 166 | Left = 416 167 | Top = 150 168 | Width = 409 169 | Height = 130 170 | Lines.Strings = ( 171 | 'My Message') 172 | TabOrder = 3 173 | end 174 | object OpenDialog1: TOpenDialog 175 | Filter = '*.pem' 176 | Left = 336 177 | Top = 8 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /Example.Windows/FMain.pas: -------------------------------------------------------------------------------- 1 | unit FMain; 2 | 3 | interface 4 | 5 | uses 6 | Winapi.Windows, 7 | Winapi.Messages, 8 | System.SysUtils, 9 | System.Variants, 10 | System.Classes, 11 | System.IOUtils, 12 | Vcl.Graphics, 13 | Vcl.Controls, 14 | Vcl.Forms, 15 | Vcl.Dialogs, 16 | Vcl.StdCtrls, 17 | Grijjy.RemotePush.Sender; 18 | 19 | type 20 | TFormMain = class(TForm) 21 | ButtonSend: TButton; 22 | GroupBoxPlatform: TGroupBox; 23 | RadioButtoniOS: TRadioButton; 24 | RadioButtonAndroid: TRadioButton; 25 | EditDeviceToken: TEdit; 26 | LabelDeviceToken: TLabel; 27 | EditTitle: TEdit; 28 | LabelTitle: TLabel; 29 | LabelMessage: TLabel; 30 | MemoMessage: TMemo; 31 | EditAndroidAPIKey: TEdit; 32 | LabelAndroidAPIKey: TLabel; 33 | EditAPNSTopic: TEdit; 34 | LabelAPNSTopic: TLabel; 35 | EditAPNSCertificate: TEdit; 36 | LabelAPNSCertificate: TLabel; 37 | EditAPNSKey: TEdit; 38 | LabelAPNSKey: TLabel; 39 | ButtonBrowseCertificate: TButton; 40 | ButtonBrowseKey: TButton; 41 | OpenDialog1: TOpenDialog; 42 | procedure ButtonSendClick(Sender: TObject); 43 | procedure ButtonBrowseCertificateClick(Sender: TObject); 44 | procedure ButtonBrowseKeyClick(Sender: TObject); 45 | procedure RadioButtoniOSClick(Sender: TObject); 46 | procedure RadioButtonAndroidClick(Sender: TObject); 47 | procedure FormShow(Sender: TObject); 48 | private 49 | procedure UpdateControls; 50 | { Private declarations } 51 | public 52 | { Public declarations } 53 | end; 54 | 55 | var 56 | FormMain: TFormMain; 57 | 58 | implementation 59 | 60 | {$R *.dfm} 61 | 62 | procedure TFormMain.UpdateControls; 63 | begin 64 | EditAPNSTopic.Enabled := RadioButtoniOS.Checked; 65 | EditAPNSCertificate.Enabled := RadioButtoniOS.Checked; 66 | EditAPNSKey.Enabled := RadioButtoniOS.Checked; 67 | ButtonBrowseCertificate.Enabled := RadioButtoniOS.Checked; 68 | ButtonBrowseKey.Enabled := RadioButtoniOS.Checked; 69 | EditAndroidAPIKey.Enabled := RadioButtonAndroid.Checked; 70 | end; 71 | 72 | procedure TFormMain.FormShow(Sender: TObject); 73 | begin 74 | UpdateControls; 75 | end; 76 | 77 | procedure TFormMain.RadioButtonAndroidClick(Sender: TObject); 78 | begin 79 | UpdateControls; 80 | end; 81 | 82 | procedure TFormMain.RadioButtoniOSClick(Sender: TObject); 83 | begin 84 | UpdateControls; 85 | end; 86 | 87 | procedure TFormMain.ButtonBrowseCertificateClick(Sender: TObject); 88 | begin 89 | if OpenDialog1.Execute then 90 | EditAPNSCertificate.Text := OpenDialog1.FileName; 91 | end; 92 | 93 | procedure TFormMain.ButtonBrowseKeyClick(Sender: TObject); 94 | begin 95 | if OpenDialog1.Execute then 96 | EditAPNSKey.Text := OpenDialog1.FileName; 97 | end; 98 | 99 | procedure TFormMain.ButtonSendClick(Sender: TObject); 100 | var 101 | RemotePushSender: TgoRemotePushSender; 102 | APlatform: TOSVersion.TPlatform; 103 | begin 104 | RemotePushSender := TgoRemotePushSender.Create; 105 | try 106 | if RadioButtoniOS.Checked then 107 | begin 108 | APlatform := TOSVersion.TPlatform.pfiOS; 109 | RemotePushSender.APNSTopic := EditAPNSTopic.Text; 110 | RemotePushSender.APNSCertificate := TFile.ReadAllBytes(EditAPNSCertificate.Text); 111 | RemotePushSender.APNSKey := TFile.ReadAllBytes(EditAPNSKey.Text); 112 | end 113 | else 114 | begin 115 | APlatform := TOSVersion.TPlatform.pfAndroid; 116 | RemotePushSender.AndroidAPIKey := EditAndroidAPIKey.Text; 117 | end; 118 | RemotePushSender.Send( 119 | APlatform, 120 | EditDeviceToken.Text, 121 | EditTitle.Text, 122 | MemoMessage.Text); 123 | finally 124 | RemotePushSender.Free; 125 | end; 126 | end; 127 | 128 | end. 129 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | TgoRemotePushSender and DelphiRemotePushSender are licensed under the Simplified BSD License. 2 | 3 | ------------------------------------------------------------------------------- 4 | 5 | Copyright (c) 2017 by Grijjy, Inc. 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Nghttp2.pas: -------------------------------------------------------------------------------- 1 | unit Nghttp2; 2 | 3 | { Header translation of ngHttp library for HTTP/2 protocol support, see https://nghttp2.org } 4 | 5 | interface 6 | 7 | const 8 | {$IF Defined(MSWINDOWS)} 9 | NGHTTP2_LIB = 'nghttp2.dll'; 10 | {$ELSEIF Defined(LINUX)} 11 | NGHTTP2_LIB = 'libnghttp2.so'; 12 | {$ENDIF} 13 | 14 | const 15 | NGHTTP2_NO_ERROR = 0; 16 | 17 | NGHTTP2_ERR_CALLBACK_FAILURE = -902; 18 | 19 | NGHTTP2_DATA = 0; 20 | _NGHTTP2_HEADERS = 1; 21 | 22 | NGHTTP2_HCAT_REQUEST = 0; 23 | NGHTTP2_HCAT_RESPONSE = 1; 24 | 25 | NGHTTP2_NV_FLAG_NONE = 0; 26 | NGHTTP2_NV_FLAG_NO_INDEX = 1; 27 | NGHTTP2_NV_FLAG_NO_COPY_NAME = 2; 28 | NGHTTP2_NV_FLAG_NO_COPY_VALUE = 4; 29 | 30 | NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS = 3; 31 | 32 | NGHTTP2_FLAG_NONE = 0; 33 | 34 | NGHTTP2_DATA_FLAG_NONE = 0; 35 | NGHTTP2_DATA_FLAG_EOF = 1; 36 | 37 | type 38 | size_t = NativeUInt; 39 | ssize_t = NativeInt; 40 | puint8 = ^uint8; 41 | puint32 = ^uint32; 42 | 43 | type 44 | { The primary structure to hold the resources needed for a HTTP/2 45 | session. The details of this structure are intentionally hidden 46 | from the public API. } 47 | pnghttp2_session = Pointer; 48 | 49 | { Callback functions for :type:`nghttp2_session`. The details of 50 | this structure are intentionally hidden from the public API. } 51 | pnghttp2_session_callbacks = Pointer; 52 | 53 | { The name/value pair, which mainly used to represent header fields. } 54 | nghttp2_nv = record 55 | name : MarshaledAString; 56 | value: MarshaledAString; 57 | namelen: size_t; 58 | valuelen: size_t; 59 | flags: uint8; 60 | end; 61 | pnghttp2_nv = ^nghttp2_nv; 62 | 63 | { The stream ID of the stream to depend on. Specifying 0 makes stream 64 | not depend any other stream. } 65 | nghttp2_priority_spec = record 66 | stream_id: int32; 67 | weight: int32; 68 | exclusive: uint8; 69 | end; 70 | pnghttp2_priority_spec = ^nghttp2_priority_spec; 71 | 72 | { The SETTINGS ID/Value pair } 73 | nghttp2_settings_entry = record 74 | settings_id: int32; 75 | value: uint32; 76 | end; 77 | pnghttp2_settings_entry = ^nghttp2_settings_entry; 78 | 79 | { The frame header } 80 | nghttp2_frame_hd = record 81 | length: size_t; 82 | stream_id: int32; 83 | &type: uint8; 84 | flags: uint8; 85 | reserved: uint8; 86 | end; 87 | 88 | { The HEADERS frame } 89 | nghttp2_headers = record 90 | hd: nghttp2_frame_hd; 91 | padlen: size_t; 92 | pri_spec: nghttp2_priority_spec; 93 | nva: pnghttp2_nv; 94 | nvlen: size_t; 95 | cat: Integer; { enum } 96 | end; 97 | 98 | { This union includes all frames to pass them to various function 99 | calls as nghttp2_frame type. } 100 | nghttp2_frame = record 101 | case Integer of 102 | 0:(hd: nghttp2_frame_hd); 103 | 1:(headers: nghttp2_headers); 104 | // nghttp2_frame_hd hd; 105 | // nghttp2_data data; 106 | // nghttp2_headers headers; 107 | // nghttp2_priority priority; 108 | // nghttp2_rst_stream rst_stream; 109 | // nghttp2_settings settings; 110 | // nghttp2_push_promise push_promise; 111 | // nghttp2_ping ping; 112 | // nghttp2_goaway goaway; 113 | // nghttp2_window_update window_update; 114 | // nghttp2_extension ext; 115 | end; 116 | pnghttp2_frame = ^nghttp2_frame; 117 | 118 | type 119 | { nghttp2_data_source_read_callback } 120 | 121 | { This union represents the some kind of data source passed to 122 | :type:`nghttp2_data_source_read_callback` } 123 | nghttp2_data_source = record 124 | case Integer of 125 | 0:(fd: Integer); 126 | 1:(ptr: Pointer); 127 | end; 128 | pnghttp2_data_source = ^nghttp2_data_source; 129 | 130 | { Callback function invoked when the library wants to read data from 131 | the |source|. The read data is sent in the stream |stream_id|. 132 | The implementation of this function must read at most |length| 133 | bytes of data from |source| (or possibly other places) and store 134 | them in |buf| and return number of data stored in |buf|. If EOF is 135 | reached, set :enum:`NGHTTP2_DATA_FLAG_EOF` flag in |*data_flags|. } 136 | nghttp2_data_source_read_callback = function(session: pnghttp2_session; 137 | stream_id: int32; buf: puint8; length: size_t; 138 | data_flags: puint32; source: pnghttp2_data_source; user_data: Pointer): ssize_t; cdecl; 139 | 140 | { This struct represents the data source and the way to read a chunk 141 | of data from it. } 142 | nghttp2_data_provider = record 143 | source: nghttp2_data_source; 144 | read_callback: nghttp2_data_source_read_callback; 145 | end; 146 | pnghttp2_data_provider = ^nghttp2_data_provider; 147 | 148 | type 149 | { nghttp2_on_header_callback } 150 | 151 | { Callback function invoked when a header name/value pair is received 152 | for the |frame|. The |name| of length |namelen| is header name. 153 | The |value| of length |valuelen| is header value. The |flags| is 154 | bitwise OR of one or more of :type:`nghttp2_nv_flag`. } 155 | nghttp2_on_header_callback = function(session: pnghttp2_session; const frame: pnghttp2_frame; 156 | const name: puint8; namelen: size_t; const value: puint8; valuelen: size_t; 157 | flags: uint8; user_data: Pointer): Integer; cdecl; 158 | 159 | type 160 | { nghttp2_on_frame_recv_callback } 161 | 162 | { Callback function invoked by `nghttp2_session_recv()` and 163 | `nghttp2_session_mem_recv()` when a frame is received. The 164 | |user_data| pointer is the third argument passed in to the call to 165 | `nghttp2_session_client_new()` or `nghttp2_session_server_new()`. } 166 | nghttp2_on_frame_recv_callback = function(session: pnghttp2_session; 167 | const frame: pnghttp2_frame; user_data: Pointer): Integer; cdecl; 168 | 169 | type 170 | { nghttp2_on_data_chunk_recv_callback } 171 | 172 | { Callback function invoked when a chunk of data in DATA frame is 173 | received. The |stream_id| is the stream ID this DATA frame belongs to. } 174 | nghttp2_on_data_chunk_recv_callback = function(session: pnghttp2_session; 175 | flags: uint8; stream_id: int32; const data: puint8; len: size_t; 176 | user_data: Pointer): Integer; cdecl; 177 | 178 | type 179 | { nghttp2_on_stream_close_callback } 180 | 181 | { Callback function invoked when the stream |stream_id| is closed. 182 | The reason of closure is indicated by the |error_code| } 183 | nghttp2_on_stream_close_callback = function(session: pnghttp2_session; 184 | stream_id: int32; error_code: uint32; user_data: Pointer): Integer; cdecl; 185 | 186 | var 187 | { Stores local settings and submits SETTINGS frame. The |iv| is the 188 | pointer to the array of :type:`nghttp2_settings_entry`. The |niv| 189 | indicates the number of :type:`nghttp2_settings_entry`. } 190 | nghttp2_submit_settings: function(session: pnghttp2_session; flags: uint8; 191 | const iv: pnghttp2_settings_entry; niv: size_t): Integer; cdecl = nil; 192 | 193 | { Initializes |*session_ptr| for client use. The all members of |callbacks| 194 | are copied to |*session_ptr|. Therefore |*session_ptr| does not store 195 | |callbacks|. The |user_data| is an arbitrary user supplied data, which 196 | will be passed to the callback functions. } 197 | nghttp2_session_client_new: function(var session_ptr: pnghttp2_session; 198 | const callbacks: pnghttp2_session_callbacks; user_data: Pointer): Integer; cdecl = nil; 199 | 200 | { Initializes |*callbacks_ptr| with NULL values. } 201 | nghttp2_session_callbacks_new: function(out callbacks_ptr: pnghttp2_session_callbacks): Integer; cdecl = nil; 202 | 203 | { Frees any resources allocated for |callbacks|. If |callbacks| is ``NULL``, this 204 | function does nothing. } 205 | nghttp2_session_callbacks_del: procedure(callbacks: pnghttp2_session_callbacks); cdecl = nil; 206 | 207 | { Sets callback function invoked when a header name/value pair is received. } 208 | nghttp2_session_callbacks_set_on_header_callback: procedure(callbacks: pnghttp2_session_callbacks; 209 | on_header_callback: nghttp2_on_header_callback); cdecl = nil; 210 | 211 | { Sets callback function invoked by `nghttp2_session_recv()` and `nghttp2_session_mem_recv()` 212 | when a frame is received. } 213 | nghttp2_session_callbacks_set_on_frame_recv_callback: procedure(callbacks: pnghttp2_session_callbacks; 214 | on_frame_recv_callback: nghttp2_on_frame_recv_callback); cdecl = nil; 215 | 216 | { Sets callback function invoked when a chunk of data in DATA frame is received. } 217 | nghttp2_session_callbacks_set_on_data_chunk_recv_callback: procedure(callbacks: pnghttp2_session_callbacks; 218 | on_data_chunk_recv_callback: nghttp2_on_data_chunk_recv_callback); cdecl = nil; 219 | 220 | { Sets callback function invoked when the stream is closed. } 221 | nghttp2_session_callbacks_set_on_stream_close_callback: procedure(callbacks: pnghttp2_session_callbacks; 222 | on_stream_close_callback: nghttp2_on_stream_close_callback); cdecl = nil; 223 | 224 | { Signals the session so that the connection should be terminated. } 225 | nghttp2_session_terminate_session: function(session: pnghttp2_session; error_code: uint32): Integer; cdecl = nil; 226 | 227 | { Submits HEADERS frame and optionally one or more DATA frames. } 228 | nghttp2_submit_request: function(session: pnghttp2_session; const pri_spec: pnghttp2_priority_spec; 229 | const nva: pnghttp2_nv; nvlen: size_t; const data_prd: pnghttp2_data_provider; stream_user_data: Pointer): int32; cdecl = nil; 230 | 231 | { Returns stream_user_data for the stream |stream_id|. } 232 | nghttp2_session_get_stream_user_data: function(session: pnghttp2_session; 233 | stream_id: int32): Pointer; cdecl = nil; 234 | 235 | { Processes data |in| as an input from the remote endpoint. The 236 | |inlen| indicates the number of bytes in the |in|. } 237 | nghttp2_session_mem_recv: function(session: pnghttp2_session; const &in: Pointer; const inlen: size_t): Integer; cdecl = nil; 238 | 239 | { Returns the serialized data to send. } 240 | nghttp2_session_mem_send: function(session: pnghttp2_session; out data_ptr: Pointer): Integer; cdecl = nil; 241 | 242 | { Returns nonzero value if |session| wants to receive data from the 243 | remote peer. If both `nghttp2_session_want_read()` and `nghttp2_session_want_write()` 244 | return 0, the application should drop the connection. } 245 | nghttp2_session_want_read: function(session: pnghttp2_session): Integer; cdecl = nil; 246 | 247 | { Returns nonzero value if |session| wants to send data to the remote 248 | peer. If both `nghttp2_session_want_read()` and * `nghttp2_session_want_write()` 249 | return 0, the application should drop the connection. } 250 | nghttp2_session_want_write: function(session: pnghttp2_session): Integer; cdecl = nil; 251 | 252 | //function MAKE_NV2(name, value: MarshaledAString): nghttp2_nv; 253 | //function MAKE_NV(name, value: MarshaledAString; valuelen: uint8): nghttp2_nv; 254 | 255 | implementation 256 | 257 | uses 258 | Classes, 259 | {$IFDEF MSWINDOWS} 260 | Windows, 261 | {$ENDIF} 262 | SyncObjs, 263 | SysUtils; 264 | 265 | var 266 | NGHTTP2Handle: HMODULE; 267 | 268 | { Helpers } 269 | 270 | //function MAKE_NV2(name, value: MarshaledAString): nghttp2_nv; 271 | //begin 272 | // Result.name := name; 273 | // Result.value := value; 274 | // Result.namelen := StrLen(name); 275 | // Result.valuelen := StrLen(value); 276 | // Result.flags := NGHTTP2_NV_FLAG_NONE; 277 | //end; 278 | // 279 | //function MAKE_NV(name, value: MarshaledAString; valuelen: uint8): nghttp2_nv; 280 | //begin 281 | // Result.name := name; 282 | // Result.value := value; 283 | // Result.namelen := StrLen(name); 284 | // Result.valuelen := valuelen; 285 | // Result.flags := NGHTTP2_NV_FLAG_NONE; 286 | //end; 287 | 288 | { Library } 289 | 290 | function LoadLib(const ALibFile: String): HMODULE; 291 | begin 292 | Result := LoadLibrary(PChar(ALibFile)); 293 | if (Result = 0) then 294 | raise Exception.CreateFmt('load %s failed', [ALibFile]); 295 | end; 296 | 297 | function FreeLib(ALibModule: HMODULE): Boolean; 298 | begin 299 | Result := FreeLibrary(ALibModule); 300 | end; 301 | 302 | function GetProc(AModule: HMODULE; const AProcName: String): Pointer; 303 | begin 304 | Result := GetProcAddress(AModule, PChar(AProcName)); 305 | if (Result = nil) then 306 | raise Exception.CreateFmt('%s is not found', [AProcName]); 307 | end; 308 | 309 | procedure LoadNGHTTP2; 310 | begin 311 | if (NGHTTP2Handle <> 0) then Exit; 312 | NGHTTP2Handle := LoadLib(NGHTTP2_LIB); 313 | if (NGHTTP2Handle = 0) then 314 | begin 315 | raise Exception.CreateFmt('Load %s failed', [NGHTTP2_LIB]); 316 | Exit; 317 | end; 318 | 319 | nghttp2_submit_settings := GetProc(NGHTTP2Handle, 'nghttp2_submit_settings'); 320 | nghttp2_session_client_new := GetProc(NGHTTP2Handle, 'nghttp2_session_client_new'); 321 | nghttp2_session_callbacks_new := GetProc(NGHTTP2Handle, 'nghttp2_session_callbacks_new'); 322 | nghttp2_session_callbacks_del := GetProc(NGHTTP2Handle, 'nghttp2_session_callbacks_del'); 323 | nghttp2_session_callbacks_set_on_header_callback := GetProc(NGHTTP2Handle, 'nghttp2_session_callbacks_set_on_header_callback'); 324 | nghttp2_session_callbacks_set_on_frame_recv_callback := GetProc(NGHTTP2Handle, 'nghttp2_session_callbacks_set_on_frame_recv_callback'); 325 | nghttp2_session_callbacks_set_on_data_chunk_recv_callback := GetProc(NGHTTP2Handle, 'nghttp2_session_callbacks_set_on_data_chunk_recv_callback'); 326 | nghttp2_session_callbacks_set_on_stream_close_callback := GetProc(NGHTTP2Handle, 'nghttp2_session_callbacks_set_on_stream_close_callback'); 327 | nghttp2_session_terminate_session := GetProc(NGHTTP2Handle, 'nghttp2_session_terminate_session'); 328 | 329 | nghttp2_submit_request := GetProc(NGHTTP2Handle, 'nghttp2_submit_request'); 330 | nghttp2_session_get_stream_user_data := GetProc(NGHTTP2Handle, 'nghttp2_session_get_stream_user_data'); 331 | 332 | nghttp2_session_mem_recv := GetProc(NGHTTP2Handle, 'nghttp2_session_mem_recv'); 333 | nghttp2_session_mem_send := GetProc(NGHTTP2Handle, 'nghttp2_session_mem_send'); 334 | nghttp2_session_want_read := GetProc(NGHTTP2Handle, 'nghttp2_session_want_read'); 335 | nghttp2_session_want_write := GetProc(NGHTTP2Handle, 'nghttp2_session_want_write'); 336 | end; 337 | 338 | procedure UnloadNGHTTP2; 339 | begin 340 | if (NGHTTP2Handle = 0) then Exit; 341 | FreeLib(NGHTTP2Handle); 342 | NGHTTP2Handle := 0; 343 | end; 344 | 345 | initialization 346 | LoadNGHTTP2; 347 | 348 | finalization 349 | UnloadNGHTTP2; 350 | 351 | end. 352 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sending iOS (and Android) remote push notifications from your Delphi service with the HTTP/2 protocol 2 | 3 | In this article we are going to discuss how to make a base unifying class for sending remote push notifications from your service to both the iOS (Apple Push Notification Service) and Android (Firebase Cloud Messaging) as well as covering how to use the newer Apple HTTP/2 protocol interface for sending push notifications from Delphi. 4 | 5 | # Introduction 6 | 7 | If you are authoring a Delphi service for the cloud, you may have the need to send push notifications from your service to your client application. There are various ways to accomplish this including using third-party BAAS providers or rolling your own interface with some of the established libraries. 8 | 9 | In the past Apple used a custom TCP binary protocol to send messages to the APNS. This was cumbersome and inflexible and had numerous pitfalls in determining whether your message was actually accepted by the interface. Apple recently released a newer interface that uses HTTP/2. The nice thing about this interface is, just like FCM it utilizes JSON for the payload of the protocol. However, the protocol uses the newer HTTP/2 protocol and ALPN (Application-Layer Protocol Negotiation Extension, defined in RFC 7301) which is required in your SSL handshake. 10 | 11 | One other advantage, Apple also increased the maximium size of the payload from 2K to 4K with the new HTTP/2 interface as of iOS 9 devices. 12 | 13 | We will show how to combine the [Grijjy scalable client socket library](https://blog.grijjy.com/2017/01/09/scalable-https-and-tcp-client-sockets-for-the-cloud) with the excellent [nghttp2](http://nghttp2.org) to solve both of these issues and transparently send push notifications to either Apple's APNS or Google's FCM from your Delphi service. 14 | 15 | For more information about us, our support and services visit the [Grijjy homepage](http://www.grijjy.com) or the [Grijjy developers blog](http://blog.grijjy.com). 16 | 17 | The example contained here depends upon part of our [Grijjy Foundation library](https://github.com/grijjy/GrijjyFoundation). 18 | 19 | The source code and related example repository is hosted on GitHub at [https://github.com/grijjy/DelphiRemotePushSender](https://github.com/grijjy/DelphiRemotePushSender). 20 | 21 | # nghttp2 22 | 23 | [Nghttp2 is a fairly established library](http://nghttp2.org) for implementing the HTTP/2 protocol. It handles all the compression and bitstreaming for implementing HTTP/2. 24 | 25 | The [HTTP/2 protocol](https://http2.github.io/) and its header compression are rather complex and evolving so it is probably best not to try and implement this ourselves. 26 | 27 | One of the great things about nghttp2 is you can utilize it entirely with memory buffers so it is a good match for our own [scalable client socket classes](https://github.com/grijjy/DelphiScalableClientSockets) for Windows and Linux. This way we get the benefit of HTTP/2 but we don't have to rely on another implementation of sockets for scaling up our service. 28 | 29 | ## Building nghttp2 30 | 31 | If you want to build the nghttp2 library for Windows to use in your Delphi application you will need to download the [latest source](https://github.com/nghttp2/nghttp2). To build you can use Visual Studio or just [download the Visual Studio Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools). You will also need to download and install [CMAKE](https://cmake.org/). 32 | 33 | 1. Download the latest nghttp2 source from https://github.com/nghttp2/nghttp2 34 | 2. Install CMAKE and the Build Tools for Visual Studio. 35 | 3. Run CMAKE followed by a period 36 | `cmake .` 37 | 4. Run CMAKE to build the release version 38 | `cmake --build . --config RELEASE` 39 | 40 | ## Delphi header translation 41 | Once completed you will have a nghttp2.dll. We will need our [conversion for the header file for Delphi](https://github.com/grijjy/DelphiRemotePushSender/blob/master/Nghttp2.pas) so we can use the nghttp2 methods directly. 42 | 43 | ## Apple APNS & iOS Prerequisites 44 | To use the `TgoRemotePushSender` class with Apple's Push Notification Service, we need to create a certificate and convert that certificate into 2 PEM files, one for the certificate and one the private key. 45 | 46 | There are quite a few primers on the Internet about how to create APNS certificates, so we will be brief here as it applies to our `TgoRemotePushSender` class. 47 | 48 | 1. On iOS you must create a production certificate that is universal by selecting the option "Apple Push Notification service SSL (Sandbox & Production)" under Certificates in the developer portal. This certificate must be matched with the proper AppId. 49 | 50 | 2. You need to download and install the certificate into the Mac OSX Keychain. 51 | 52 | 3. You need to export the Apple Push Notification service certificate and private key as 2 separate files (cert.p12 and key.p12) from the Keychain application on OSX. These can only be exported as .p12 files from the Keychain application. 53 | 54 | To create PEM files we recommend using the OpenSSL binaries to convert as follows: 55 | ```shell 56 | openssl pkcs12 -clcerts -nokeys -out APNS-cert.pem -in cert.p12 57 | ``` 58 | ```shell 59 | openssl pkcs12 -nocerts -out APNS-key-password.pem -in key.p12 60 | ``` 61 | To remove the password from the APNS-key-password.pem file: 62 | ```shell 63 | openssl rsa -in APNS-key-password.pem -out APNS-key.pem 64 | ``` 65 | To test the certificate against the APNS interface to see if it works: 66 | ```shell 67 | openssl s_client -connect gateway.push.apple.com:2195 -cert APNS-cert.pem -key APNS-key.pem 68 | ``` 69 | # TgoRemotePushSender 70 | 71 | To use the `TgoRemotePushSender` class we need to know the Device Token for the target user's device. You pass this value to the `TgoRemotePushSender.Send` method when sending your remote push notification. This is true of both iOS and Android targets. 72 | 73 | For Android you also need to specify the `TgoRemotePushSender.AndroidAPIKey`. You can obtain an API Key by setting up cloud messaging on [Google's developer site](https://developers.google.com/cloud-messaging/). This is also called the Server API Key under Firebase Cloud Messaging. 74 | 75 | For iOS you need to specify the `TgoRemotePushSender.APNSKey` which is essentially your Bundle Identifier (ex: com.mycool.app). You also need to apply the APNS certificate and private key .PEM files you previously created. 76 | ```Delphi 77 | TgoRemotePushSender.APNSKey := 'com.mycool.app'; 78 | TgoRemotePushSender.APNSCertificate := TFile.ReadAllBytes(PathToCertificate.PEM); 79 | TgoRemotePushSender.APNSKey := TFile.ReadAllBytes(PathToKey.PEM); 80 | ``` 81 | 82 | The structure of the class is fairly straightforward. To send a remote push notification, call the `Send` method and provide the `DeviceToken` and also the `Title` and `Message` body for the payload. 83 | 84 | ```Delphi 85 | TgoRemotePushSender = class(TObject) 86 | public 87 | constructor Create; 88 | destructor Destroy; override; 89 | public 90 | { Send push notification } 91 | function Send(const APlatform: TOSVersion.TPlatform; const ADeviceToken: String; 92 | const ATitle, AMessage: String): Boolean; 93 | public 94 | { Android API Key for your app } 95 | property AndroidAPIKey: String read FAndroidAPIKey write FAndroidAPIKey; 96 | 97 | { iOS Certificate } 98 | property APNSCertificate: TBytes read FAPNSCertificate write FAPNSCertificate; 99 | 100 | { iOS Key } 101 | property APNSKey: TBytes read FAPNSKey write FAPNSKey; 102 | 103 | { iOS Topic } 104 | property APNSTopic: String read FAPNSTopic write FAPNSTopic; 105 | end; 106 | ``` 107 | 108 | > Note: You should create and reuse an instance of this class to avoid creating multiple connections to the push notification host. One model would be to perform notifications in batches based upon time. Apple warns about creating too many connections as a reason for blocking your service. 109 | 110 | [An example application](https://github.com/grijjy/DelphiRemotePushSender) demonstrating how to use this class is available within our repository on GitHub. 111 | 112 | # Sending custom user data 113 | 114 | Now that both Apple and Google use JSON as their payload format, you can easily add your own custom JSON fields to the payload that are consistent with both platforms. While we don't expose this directly in our class you can add additional content by simply expanding the JSON with your extra custom data. 115 | 116 | # License 117 | 118 | TgoRemotePushSender and DelphiRemotePushSender are licensed under the Simplified BSD License. See License.txt for details. -------------------------------------------------------------------------------- /ReceivingPushNotifications.md: -------------------------------------------------------------------------------- 1 | # Receiving push notifications from Apple's Push Notification and Google's Firebase Cloud Messaging services 2 | 3 | In the [previous article](https://blog.grijjy.com/2017/01/18/sending-ios-and-android-remote-push-notifications-from-your-delphi-service-with-the-http2-protocol/) we demonstrated how to roll your own Google Firebase Cloud Messaging and Apple APNS push notifications from Delphi apps in a unified manner. This article continues the conversation about how to easily receive and consume them in your Delphi mobile app. 4 | 5 | # Introduction 6 | If you are authoring a Delphi service for the cloud, you may have the need to send and receive push notifications from your service to your mobile client application. You could use an existing BAAS provider to accomplish this, but why pay for this when you can roll your own so easily with Delphi? 7 | 8 | Delphi provides a base set of classes in the units `FMX.PushNotification.Android` and `FMX.PushNotification.iOS` that do most of the heavy lifting and allow you to easily consume remote push notifications. In this conversation we will be demonstrating a helper class that unifies the approach for both platforms, uses TMessage callbacks for notifications and device token changes and adds some new missing features. 9 | 10 | For more information about us, our support and services visit the [Grijjy homepage](http://www.grijjy.com) or the [Grijjy developers blog](http://blog.grijjy.com). 11 | 12 | The example contained here depends upon part of our [Grijjy Foundation library](https://github.com/grijjy/GrijjyFoundation). 13 | 14 | # Getting started on iOS 15 | To receive push notifications on iOS you must: 16 | 17 | 1. Already have created an [Apple Push Services certificate in the Apple Developer website](https://developer.apple.com/account/ios/certificate/) for use in sending remote push notifications. 18 | 19 | 2. Create an App ID on the Apple Developer website for your project. 20 | 21 | 3. Enable Push Notifications for your App ID on the Apple Developer website for your project. 22 | 23 | 4. A Provisioning Profile must exist on the Apple website that matches the identifier. 24 | 25 | Then in your Delphi Project Options you must set your CFBundleIdentifier for iOS build configurations to the matching name of the ID in Apple Developer website. (ex: com.some.app) This is the only thing your app will need to identify itself for Apple Push Notifications. 26 | 27 | # Getting started on Android 28 | To receive push notifications on Android you must: 29 | 30 | 1. Obtain a Firebase Cloud Messaging API Key by setting up cloud messaging on [Google's developer site](https://developers.google.com/cloud-messaging/). This is also called the Server API Key under Firebase Cloud Messaging. 31 | 32 | 2. Modify your AndroidManifest.xml to register an Intent and add the following receiver: 33 | 34 | ```xml 35 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | <%receivers%> 45 | ``` 46 | For Android, we only need the Firebase Cloud Messaging API Key for our Delphi app. 47 | 48 | # TgoRemotePushReceiver class 49 | The [TgoRemotePushReceiver class](https://github.com/grijjy/GrijjyFoundation/blob/master/Grijjy.RemotePush.Receiver.pas) unifies the receiving of remote push notifications from both APNS and Firebase Cloud Messaging into a common class model. 50 | ```Delphi 51 | TgoRemotePushReceiver = class(TObject) 52 | public 53 | constructor Create(const AGCMAppId: UnicodeString); 54 | destructor Destroy; override; 55 | public 56 | property DeviceToken: String read FDeviceToken; 57 | property Number: Integer read GetNumber write SetNumber; 58 | end; 59 | ``` 60 | 61 | When you receive a remote push notification message from Android or iOS, the following TMessage is fired: 62 | ```Delphi 63 | TgoRemoteNotificationMessage = class(TMessage) 64 | public 65 | constructor Create(const ADataKey, AJson: String; const AActivated: Boolean); 66 | property DataKey: String read FDataKey; 67 | property Json: String read FJson; 68 | 69 | { Whether message is activated by the user (tapping in it) } 70 | property Activated: Boolean read FActivated; 71 | end; 72 | ``` 73 | Since we are receiving a JSON payload for both Android and iOS we can examine the content here. This is useful is we want to include our own custom content in the payload that is app specific. See our [article on sending remote push notifications](https://blog.grijjy.com/2017/01/18/sending-ios-and-android-remote-push-notifications-from-your-delphi-service-with-the-http2-protocol/) for a discussion about customizing the payload. 74 | 75 | If your device token changes on Android or iOS, the following TMessage is fired: 76 | ```Delphi 77 | { Device token change message } 78 | TgoDeviceTokenChangeMessage = class(TMessage) 79 | private 80 | FDeviceToken: String; 81 | public 82 | constructor Create(const ADeviceToken: String); 83 | property DeviceToken: String read FDeviceToken; 84 | end; 85 | ``` 86 | The above message is fired initially when you device token is assigned after app startup. Due to the variances in the way device tokens are handled on iOS and Android this event could be immediate or delayed by the operating system. 87 | 88 | As a developer you should also be prepared that the device token could change if your application is reinstalled, the operating system is updated as well as dynamically while your app is active. 89 | 90 | # Apple APNS quirks 91 | On iOS Remote Push notification events are received only when your application is in the foreground or background. If your application is "force quit" also known as "terminated" then you cannot receive remote push notification events into your app. Instead when the push notification arrives on screen and is pressed your app is launched but without receiving a notification event. 92 | 93 | > Note: You only receive a single remote notification event at a time, meaning that if there are several events queued, the only one you receive is the one that the user clicked on in the iOS user interface. 94 | 95 | iOS 8.x introduces a new approach called the PushKit which has the ability to silently launch your app into the background state. We will explore this in a future article. 96 | 97 | # Hello World for push notifications 98 | To get started using the helper class, you will need your Android API Key for Firebase Cloud Messaging. 99 | 100 | ```Delphi 101 | RemotePushReceiver := TgrRemotePushReceiver.Create(MyAndroidApiKey); 102 | ``` 103 | To subscribe to remote push notifications: 104 | ```Delphi 105 | TMessageManager.DefaultManager.SubscribeToMessage(TgoRemoteNotificationMessage, RemoteNotificationHandler); 106 | ``` 107 | 108 | ```Delphi 109 | procedure TMyClass.RemoteNotificationHandler(const Sender: TObject; 110 | const Msg: TMessage); 111 | var 112 | RemoteNotificationMessage: TgrRemoteNotificationMessage; 113 | Doc: TgoBsonDocument; 114 | begin 115 | Assert(Assigned(Msg)); 116 | Assert(Msg is TgoRemoteNotificationMessage); 117 | RemoteNotificationMessage := Msg as TgoRemoteNotificationMessage; 118 | { The user tapped this push notification, which resulted in the bringing the app to the 119 | foreground? } 120 | if (RemoteNotificationMessage.Activated) then 121 | Doc := TgrBsonDocument.Parse(RemoteNotificationMessage.Json); 122 | end; 123 | ``` 124 | You will receive an Activated set to True if the notification is the likely result of a end-user actually pressing the notification. 125 | 126 | To subscribe to device token changes: 127 | ```Delphi 128 | TMessageManager.DefaultManager.SubscribeToMessage(TgoDeviceTokenChangeMessage, DeviceTokenChangeListener); 129 | ``` 130 | 131 | ```Delphi 132 | procedure TMyClass.DeviceTokenChangeListener(const Sender: TObject; 133 | const M: TMessage); 134 | var 135 | DeviceTokenChangeMessage: TgoDeviceTokenChangeMessage; 136 | begin 137 | DeviceTokenChangeMessage := M as TgoDeviceTokenChangeMessage; 138 | Writeln('Device Token is ' + DeviceTokenChangeMessage.DeviceToken); 139 | end; 140 | ``` 141 | 142 | > Make sure you unsubscribe all the various TMessage listeners when you are completed. 143 | 144 | # Conclusion 145 | Receiving remote push notifications in Delphi apps for Android and iOS is relatively straightforward with this helper class. In the end, all you need to know is the device token in order to send a message to a device. 146 | 147 | # License 148 | 149 | TgoRemotePushReceiver is licensed under the Simplified BSD License. See License.txt for details. --------------------------------------------------------------------------------