├── .nvmrc ├── .watchmanconfig ├── example ├── .watchmanconfig ├── jest.config.js ├── .bundle │ └── config ├── app.json ├── ios │ ├── File.swift │ ├── .xcode.env.local │ ├── PushExample │ │ ├── Images.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── PushExample.entitlements │ │ ├── Info.plist │ │ └── LaunchScreen.storyboard │ ├── PushExample.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── PushExample-Bridging-Header.h │ ├── .xcode.env │ ├── PushExampleTests │ │ ├── Info.plist │ │ └── PushExampleTests.m │ ├── Podfile │ ├── AppDelegate.swift │ └── PushExample.xcodeproj │ │ └── xcshareddata │ │ └── xcschemes │ │ └── PushExample.xcscheme ├── android │ ├── app │ │ ├── debug.keystore │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── values │ │ │ │ │ │ ├── strings.xml │ │ │ │ │ │ └── styles.xml │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── drawable-anydpi-v24 │ │ │ │ │ │ └── ic_stat_name.xml │ │ │ │ │ └── drawable │ │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ ├── java │ │ │ │ │ └── com │ │ │ │ │ │ └── pushexample │ │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ │ └── MainApplication.kt │ │ │ │ └── AndroidManifest.xml │ │ │ └── debug │ │ │ │ └── AndroidManifest.xml │ │ ├── proguard-rules.pro │ │ ├── google-services.json │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ ├── build.gradle │ ├── gradle.properties │ ├── gradlew.bat │ └── gradlew ├── index.js ├── react-native.config.js ├── payload.json ├── Gemfile ├── babel.config.js ├── package.json ├── metro.config.js ├── Gemfile.lock ├── README.md └── src │ └── App.tsx ├── src ├── types │ ├── index.ts │ ├── module.ts │ └── native.ts ├── apis │ ├── getConstants.ts │ ├── registerForToken.ts │ ├── setBadgeCount.ts │ ├── getBadgeCount.ts │ ├── removeListeners.ts │ ├── completeNotification.ts │ ├── getPermissionStatus.ts │ ├── addErrorListener.ts │ ├── getLaunchNotification.ts │ ├── addTokenEventListener.ts │ ├── requestPermissions.ts │ ├── addMessageEventListener.ts │ ├── registerHeadlessTask.ts │ └── index.ts ├── utils │ ├── index.ts │ ├── normalizeNativePermissionStatus.ts │ └── normalizeNativeMessage.ts ├── constants.ts ├── nativeModule.ts └── index.ts ├── .gitattributes ├── tsconfig.build.json ├── babel.config.js ├── ios ├── Push-Bridging-Header.h ├── PushNotification.h ├── PushNotification.m ├── Push.mm ├── PushNotificationAppDelegateHelper.swift ├── Status.swift ├── Push.swift └── PushHelper.swift ├── android ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── res │ │ ├── drawable-hdpi │ │ │ └── ic_default_notification.png │ │ ├── drawable-mdpi │ │ │ └── ic_default_notification.png │ │ ├── drawable-xhdpi │ │ │ └── ic_default_notification.png │ │ ├── drawable-xxhdpi │ │ │ └── ic_default_notification.png │ │ ├── values │ │ │ └── strings.xml │ │ └── drawable-anydpi-v24 │ │ │ └── ic_default_notification.xml │ │ ├── java │ │ └── com │ │ │ └── candlefinance │ │ │ └── push │ │ │ ├── PushPackage.kt │ │ │ ├── ContextHolder.kt │ │ │ ├── RNEventEmitter.kt │ │ │ ├── FirebaseMessagingService.kt │ │ │ ├── PushModule.kt │ │ │ └── NotificationUtils.kt │ │ └── AndroidManifestNew.xml ├── gradle.properties └── build.gradle ├── lefthook.yml ├── .editorconfig ├── .yarnrc.yml ├── PushNotification.h ├── tsconfig.json ├── turbo.json ├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ └── ci.yml ├── PushNotification.m ├── PushNotificationAppDelegateHelper.swift ├── .gitignore ├── LICENSE ├── candlefinance-push.podspec ├── Status.swift ├── package.json ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── README.md └── PushHelper.swift /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './native'; 3 | -------------------------------------------------------------------------------- /example/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | }; 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /example/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PushExample", 3 | "displayName": "PushExample" 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": ["example"] 4 | } 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:@react-native/babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /example/ios/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // PushExample 4 | // 5 | 6 | import Foundation 7 | -------------------------------------------------------------------------------- /ios/Push-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | -------------------------------------------------------------------------------- /example/ios/.xcode.env.local: -------------------------------------------------------------------------------- 1 | export NODE_BINARY=/Users/gary/.nvm/versions/node/v18.16.0/bin/node 2 | 3 | -------------------------------------------------------------------------------- /example/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/example/android/app/debug.keystore -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | PushExample 3 | 4 | -------------------------------------------------------------------------------- /example/ios/PushExample/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/apis/getConstants.ts: -------------------------------------------------------------------------------- 1 | import { nativeModule } from '../nativeModule'; 2 | 3 | export const getConstants = () => nativeModule.getConstants(); 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commit-msg: 4 | parallel: true 5 | commands: 6 | commitlint: 7 | run: npx commitlint --edit 8 | -------------------------------------------------------------------------------- /src/apis/registerForToken.ts: -------------------------------------------------------------------------------- 1 | import { nativeModule } from '../nativeModule'; 2 | 3 | export const registerForToken = (): void => nativeModule.registerForToken?.(); 4 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | Push_kotlinVersion=1.7.0 2 | Push_minSdkVersion=21 3 | Push_targetSdkVersion=31 4 | Push_compileSdkVersion=31 5 | Push_ndkversion=21.4.7075529 6 | -------------------------------------------------------------------------------- /android/src/main/res/drawable-hdpi/ic_default_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/android/src/main/res/drawable-hdpi/ic_default_notification.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-mdpi/ic_default_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/android/src/main/res/drawable-mdpi/ic_default_notification.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { normalizeNativeMessage } from './normalizeNativeMessage'; 2 | export { normalizeNativePermissionStatus } from './normalizeNativePermissionStatus'; 3 | -------------------------------------------------------------------------------- /android/src/main/res/drawable-xhdpi/ic_default_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/android/src/main/res/drawable-xhdpi/ic_default_notification.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-xxhdpi/ic_default_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/android/src/main/res/drawable-xxhdpi/ic_default_notification.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src/apis/setBadgeCount.ts: -------------------------------------------------------------------------------- 1 | import { nativeModule } from '../nativeModule'; 2 | 3 | export const setBadgeCount = (count: number): void => 4 | nativeModule.setBadgeCount?.(count); 5 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src/apis/getBadgeCount.ts: -------------------------------------------------------------------------------- 1 | import { nativeModule } from '../nativeModule'; 2 | 3 | export const getBadgeCount = (): void | Promise => 4 | nativeModule.getBadgeCount?.(); 5 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from 'react-native'; 2 | import App from './src/App'; 3 | import { name as appName } from './app.json'; 4 | 5 | AppRegistry.registerComponent(appName, () => App); 6 | -------------------------------------------------------------------------------- /src/apis/removeListeners.ts: -------------------------------------------------------------------------------- 1 | import { nativeEventEmitter } from '../nativeModule'; 2 | 3 | export const removeListeners = (event: string) => { 4 | nativeEventEmitter.removeAllListeners(event); 5 | }; 6 | -------------------------------------------------------------------------------- /src/apis/completeNotification.ts: -------------------------------------------------------------------------------- 1 | import { nativeModule } from '../nativeModule'; 2 | 3 | export const completeNotification = (completionHandlerId: string): void => 4 | nativeModule.completeNotification?.(completionHandlerId); 5 | -------------------------------------------------------------------------------- /android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | fcm_default_channel 3 | FCM Default Channel 4 | 5 | -------------------------------------------------------------------------------- /example/react-native.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = { 5 | dependencies: { 6 | [pak.name]: { 7 | root: path.join(__dirname, '..'), 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /example/payload.json: -------------------------------------------------------------------------------- 1 | { 2 | "aps": { 3 | "alert": { 4 | "title": "Hello World", 5 | "body": "Hello, this is a push notification." 6 | }, 7 | "content-available": 1 8 | }, 9 | "data": { 10 | "key1": "value1", 11 | "key2": "value2" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'PushExample' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | includeBuild('../node_modules/@react-native/gradle-plugin') 5 | -------------------------------------------------------------------------------- /example/ios/PushExample/PushExample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /example/ios/PushExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/PushExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | nmHoistingLimits: workspaces 3 | 4 | plugins: 5 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 6 | spec: "@yarnpkg/plugin-interactive-tools" 7 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 8 | spec: "@yarnpkg/plugin-workspace-tools" 9 | 10 | yarnPath: .yarn/releases/yarn-3.6.1.cjs 11 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /src/apis/getPermissionStatus.ts: -------------------------------------------------------------------------------- 1 | import { nativeModule } from '../nativeModule'; 2 | import type { PushNotificationPermissionStatus } from '../types'; 3 | import { normalizeNativePermissionStatus } from '../utils'; 4 | 5 | export const getPermissionStatus = 6 | async (): Promise => 7 | normalizeNativePermissionStatus(await nativeModule.getPermissionStatus()); 8 | -------------------------------------------------------------------------------- /src/apis/addErrorListener.ts: -------------------------------------------------------------------------------- 1 | import type { EmitterSubscription } from 'react-native'; 2 | 3 | import { nativeEventEmitter } from '../nativeModule'; 4 | 5 | export const addErrorListener = ( 6 | event: string, 7 | listener: (message: string) => void 8 | ): EmitterSubscription => 9 | nativeEventEmitter.addListener(event, ({ message }: { message: string }) => { 10 | listener(message); 11 | }); 12 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 2.6.10" 5 | 6 | # Cocoapods 1.15 introduced a bug which break the build. We will remove the upper 7 | # bound in the template on Cocoapods with next React Native release. 8 | gem 'cocoapods', '>= 1.13', '< 1.15' 9 | gem 'activesupport', '>= 6.1.7.5', '< 7.1.0' 10 | -------------------------------------------------------------------------------- /src/apis/getLaunchNotification.ts: -------------------------------------------------------------------------------- 1 | import { nativeModule } from '../nativeModule'; 2 | import type { PushNotificationMessage } from '../types'; 3 | import { normalizeNativeMessage } from '../utils'; 4 | 5 | export const getLaunchNotification = 6 | async (): Promise => 7 | normalizeNativeMessage( 8 | (await nativeModule.getLaunchNotification()) ?? undefined 9 | ); 10 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | 3 | // General 4 | export const PACKAGE_NAME = 'Push'; 5 | export const LINKING_ERROR = 6 | `The ${PACKAGE_NAME} package doesn't seem to be linked. Make sure: \n\n` + 7 | Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + 8 | '- You rebuilt the app after installing the package\n' + 9 | '- You are not using Expo Go\n'; 10 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = { 5 | presets: ['module:@react-native/babel-preset'], 6 | plugins: [ 7 | [ 8 | 'module-resolver', 9 | { 10 | extensions: ['.tsx', '.ts', '.js', '.json'], 11 | alias: { 12 | [pak.name]: path.join(__dirname, '..', pak.source), 13 | }, 14 | }, 15 | ], 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /src/apis/addTokenEventListener.ts: -------------------------------------------------------------------------------- 1 | import type { EmitterSubscription } from 'react-native'; 2 | 3 | import { nativeEventEmitter } from '../nativeModule'; 4 | import type { TokenPayload } from '../types'; 5 | 6 | export const addTokenEventListener = ( 7 | event: string, 8 | listener: (token: string) => void 9 | ): EmitterSubscription => 10 | nativeEventEmitter.addListener(event, ({ token }: TokenPayload) => { 11 | listener(token); 12 | }); 13 | -------------------------------------------------------------------------------- /src/apis/requestPermissions.ts: -------------------------------------------------------------------------------- 1 | import { nativeModule } from '../nativeModule'; 2 | import type { PushNotificationPermissions } from '../types'; 3 | 4 | export const requestPermissions = async ( 5 | { alert = true, badge = true, sound = true }: PushNotificationPermissions = { 6 | alert: true, 7 | badge: true, 8 | sound: true, 9 | } 10 | ): Promise => 11 | nativeModule.requestPermissions({ 12 | alert, 13 | badge, 14 | sound, 15 | }); 16 | -------------------------------------------------------------------------------- /example/ios/PushExample-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import 6 | #import 7 | #import 8 | #import 9 | #import 10 | #import 11 | #import 12 | 13 | #import 14 | #import 15 | -------------------------------------------------------------------------------- /example/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /example/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /src/nativeModule.ts: -------------------------------------------------------------------------------- 1 | import { NativeEventEmitter, NativeModules } from 'react-native'; 2 | 3 | import { LINKING_ERROR } from './constants'; 4 | import type { PushNotificationNativeModule } from './types'; 5 | 6 | export const nativeModule: PushNotificationNativeModule = NativeModules.Push 7 | ? NativeModules.Push 8 | : new Proxy( 9 | {}, 10 | { 11 | get() { 12 | throw new Error(LINKING_ERROR); 13 | }, 14 | } 15 | ); 16 | 17 | export const nativeEventEmitter = new NativeEventEmitter(nativeModule); 18 | -------------------------------------------------------------------------------- /PushNotification.h: -------------------------------------------------------------------------------- 1 | // 2 | // PushNotification.h 3 | // candlefinance-push 4 | // 5 | // Created by Gary Tokman on 4/16/24. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface PushNotification : NSObject 13 | 14 | + (void) didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken; 15 | + (void) didFailToRegisterForRemoteNotificationsWithError:(NSError*)error; 16 | + (void) didReceiveRemoteNotification:(NSDictionary *)userInfo withCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler; 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /ios/PushNotification.h: -------------------------------------------------------------------------------- 1 | // 2 | // PushNotification.h 3 | // candlefinance-push 4 | // 5 | // Created by Gary Tokman on 4/16/24. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface PushNotification : NSObject 13 | 14 | + (void) didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken; 15 | + (void) didFailToRegisterForRemoteNotificationsWithError:(NSError*)error; 16 | + (void) didReceiveRemoteNotification:(NSDictionary *)userInfo withCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler; 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /src/utils/normalizeNativePermissionStatus.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | NativePermissionStatus, 3 | PushNotificationPermissionStatus, 4 | } from '../types'; 5 | 6 | /** 7 | * @internal 8 | */ 9 | export const normalizeNativePermissionStatus = ( 10 | nativeStatus: NativePermissionStatus 11 | ): PushNotificationPermissionStatus => { 12 | switch (nativeStatus) { 13 | case 'ShouldRequest': 14 | case 'NotDetermined': 15 | case 'ShouldExplainThenRequest': 16 | return 'notDetermined'; 17 | case 'Authorized': 18 | case 'Granted': 19 | return 'granted'; 20 | case 'Denied': 21 | return 'denied'; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /android/src/main/java/com/candlefinance/push/PushPackage.kt: -------------------------------------------------------------------------------- 1 | package com.candlefinance.push 2 | 3 | import com.facebook.react.ReactPackage 4 | import com.facebook.react.bridge.NativeModule 5 | import com.facebook.react.bridge.ReactApplicationContext 6 | import com.facebook.react.uimanager.ViewManager 7 | 8 | 9 | class PushPackage : ReactPackage { 10 | override fun createNativeModules(reactContext: ReactApplicationContext): List { 11 | return listOf(PushModule(reactContext)) 12 | } 13 | 14 | override fun createViewManagers(reactContext: ReactApplicationContext): List> { 15 | return emptyList() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | buildToolsVersion = "34.0.0" 4 | minSdkVersion = 21 5 | compileSdkVersion = 34 6 | targetSdkVersion = 34 7 | ndkVersion = "25.1.8937393" 8 | kotlinVersion = "1.8.0" 9 | } 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle") 16 | classpath("com.facebook.react:react-native-gradle-plugin") 17 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") 18 | classpath("com.google.gms:google-services:4.4.1") 19 | } 20 | } 21 | 22 | apply plugin: "com.facebook.react.rootproject" 23 | -------------------------------------------------------------------------------- /src/apis/addMessageEventListener.ts: -------------------------------------------------------------------------------- 1 | import type { EmitterSubscription } from 'react-native'; 2 | 3 | import { nativeEventEmitter } from '../nativeModule'; 4 | import type { NativeMessage, PushNotificationMessage } from '../types'; 5 | import { normalizeNativeMessage } from '../utils'; 6 | 7 | export const addMessageEventListener = ( 8 | event: string, 9 | listener: ( 10 | message: PushNotificationMessage | null, 11 | completionHandlerId?: string 12 | ) => void 13 | ): EmitterSubscription => 14 | nativeEventEmitter.addListener(event, (nativeMessage: NativeMessage) => { 15 | listener( 16 | normalizeNativeMessage(nativeMessage), 17 | nativeMessage.completionHandlerId 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /src/apis/registerHeadlessTask.ts: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from 'react-native'; 2 | 3 | import type { PushNotificationMessage } from '../types'; 4 | import { normalizeNativeMessage } from '../utils'; 5 | 6 | import { getConstants } from './getConstants'; 7 | 8 | export const registerHeadlessTask = ( 9 | task: (message: PushNotificationMessage | null) => Promise 10 | ): void => { 11 | const { NativeHeadlessTaskKey } = getConstants(); 12 | if (NativeHeadlessTaskKey) { 13 | AppRegistry.registerHeadlessTask( 14 | NativeHeadlessTaskKey, 15 | () => async (nativeMessage) => { 16 | const parsed = JSON.parse(nativeMessage); 17 | await task(normalizeNativeMessage(parsed)); 18 | } 19 | ); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/apis/index.ts: -------------------------------------------------------------------------------- 1 | export { addMessageEventListener } from './addMessageEventListener'; 2 | export { addTokenEventListener } from './addTokenEventListener'; 3 | export { completeNotification } from './completeNotification'; 4 | export { getBadgeCount } from './getBadgeCount'; 5 | export { getConstants } from './getConstants'; 6 | export { getLaunchNotification } from './getLaunchNotification'; 7 | export { getPermissionStatus } from './getPermissionStatus'; 8 | export { registerHeadlessTask } from './registerHeadlessTask'; 9 | export { requestPermissions } from './requestPermissions'; 10 | export { setBadgeCount } from './setBadgeCount'; 11 | export { registerForToken } from './registerForToken'; 12 | export { removeListeners } from './removeListeners'; 13 | export { addErrorListener } from './addErrorListener'; 14 | -------------------------------------------------------------------------------- /example/android/app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "1072447464670", 4 | "project_id": "push-example-6b827", 5 | "storage_bucket": "push-example-6b827.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:1072447464670:android:0ce5ae21b1edd1d46c8e4f", 11 | "android_client_info": { 12 | "package_name": "com.pushexample" 13 | } 14 | }, 15 | "oauth_client": [], 16 | "api_key": [ 17 | { 18 | "current_key": "AIzaSyBVp1lTAM90qdxx-mgmN8kA16D-hiu375Q" 19 | } 20 | ], 21 | "services": { 22 | "appinvite_service": { 23 | "other_platform_oauth_client": [] 24 | } 25 | } 26 | } 27 | ], 28 | "configuration_version": "1" 29 | } -------------------------------------------------------------------------------- /src/types/module.ts: -------------------------------------------------------------------------------- 1 | export interface PushNotificationPermissions 2 | extends Partial> { 3 | alert?: boolean; 4 | badge?: boolean; 5 | sound?: boolean; 6 | } 7 | 8 | export interface PushNotificationMessage { 9 | title?: string; 10 | body?: string; 11 | imageUrl?: string; 12 | deeplinkUrl?: string; 13 | goToUrl?: string; 14 | fcmOptions?: FcmPlatformOptions; 15 | apnsOptions?: ApnsPlatformOptions; 16 | data?: Record; 17 | custom?: any; 18 | } 19 | 20 | export type PushNotificationPermissionStatus = 21 | | 'denied' 22 | | 'granted' 23 | | 'notDetermined'; 24 | 25 | interface ApnsPlatformOptions { 26 | subtitle?: string; 27 | } 28 | 29 | interface FcmPlatformOptions { 30 | channelId: string; 31 | messageId: string; 32 | senderId: string; 33 | sendTime: Date; 34 | } 35 | -------------------------------------------------------------------------------- /android/src/main/res/drawable-anydpi-v24/ic_default_notification.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-anydpi-v24/ic_stat_name.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "paths": { 5 | "@candlefinance/push": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "react", 12 | "lib": ["esnext"], 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitReturns": true, 17 | "noImplicitUseStrict": false, 18 | "noStrictGenericChecks": false, 19 | "noUncheckedIndexedAccess": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "resolveJsonModule": true, 23 | "skipLibCheck": true, 24 | "strict": true, 25 | "target": "esnext", 26 | "verbatimModuleSyntax": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/ios/PushExampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build:android": { 5 | "inputs": [ 6 | "package.json", 7 | "android", 8 | "!android/build", 9 | "src/*.ts", 10 | "src/*.tsx", 11 | "example/package.json", 12 | "example/android", 13 | "!example/android/.gradle", 14 | "!example/android/build", 15 | "!example/android/app/build" 16 | ], 17 | "outputs": [] 18 | }, 19 | "build:ios": { 20 | "inputs": [ 21 | "package.json", 22 | "*.podspec", 23 | "ios", 24 | "src/*.ts", 25 | "src/*.tsx", 26 | "example/package.json", 27 | "example/ios", 28 | "!example/ios/build", 29 | "!example/ios/Pods" 30 | ], 31 | "outputs": [] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /android/src/main/java/com/candlefinance/push/ContextHolder.kt: -------------------------------------------------------------------------------- 1 | package com.candlefinance.push 2 | import com.facebook.react.bridge.ReactContext 3 | 4 | class ContextHolder private constructor() { 5 | private lateinit var applicationContext: ReactContext 6 | 7 | companion object { 8 | 9 | @Volatile private var instance: ContextHolder? = null 10 | 11 | fun getInstance() = 12 | instance ?: synchronized(this) { 13 | instance ?: ContextHolder().also { instance = it } 14 | } 15 | } 16 | 17 | fun setApplicationContext(context: ReactContext) { 18 | if (!::applicationContext.isInitialized) { 19 | applicationContext = context 20 | } 21 | } 22 | 23 | fun getApplicationContext(): ReactContext? { 24 | if (!::applicationContext.isInitialized) { 25 | return null 26 | } 27 | return applicationContext 28 | } 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup Node.js and install dependencies 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Setup Node.js 8 | uses: actions/setup-node@v3 9 | with: 10 | node-version-file: .nvmrc 11 | 12 | - name: Cache dependencies 13 | id: yarn-cache 14 | uses: actions/cache@v3 15 | with: 16 | path: | 17 | **/node_modules 18 | .yarn/install-state.gz 19 | key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/package.json', '!node_modules/**') }} 20 | restore-keys: | 21 | ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} 22 | ${{ runner.os }}-yarn- 23 | 24 | - name: Install dependencies 25 | if: steps.yarn-cache.outputs.cache-hit != 'true' 26 | run: yarn install --immutable 27 | shell: bash 28 | -------------------------------------------------------------------------------- /PushNotification.m: -------------------------------------------------------------------------------- 1 | // 2 | // PushNotification.m 3 | // candlefinance-push 4 | // 5 | // Created by Gary Tokman on 4/16/24. 6 | // 7 | 8 | #import "PushNotification.h" 9 | #import "candlefinance_push-Swift.h" 10 | 11 | @implementation PushNotification 12 | 13 | + (void) didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken { 14 | [PushNotificationAppDelegateHelper didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; 15 | } 16 | 17 | + (void) didFailToRegisterForRemoteNotificationsWithError:(NSError*)error { 18 | [PushNotificationAppDelegateHelper didFailToRegisterForRemoteNotificationsWithError:error]; 19 | } 20 | 21 | + (void) didReceiveRemoteNotification:(NSDictionary*)userInfo withCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { 22 | [PushNotificationAppDelegateHelper didReceiveRemoteNotificationWithUserInfo:userInfo completionHandler:completionHandler]; 23 | } 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /ios/PushNotification.m: -------------------------------------------------------------------------------- 1 | // 2 | // PushNotification.m 3 | // candlefinance-push 4 | // 5 | // Created by Gary Tokman on 4/16/24. 6 | // 7 | 8 | #import "PushNotification.h" 9 | #import "candlefinance_push-Swift.h" 10 | 11 | @implementation PushNotification 12 | 13 | + (void) didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken { 14 | [PushNotificationAppDelegateHelper didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; 15 | } 16 | 17 | + (void) didFailToRegisterForRemoteNotificationsWithError:(NSError*)error { 18 | [PushNotificationAppDelegateHelper didFailToRegisterForRemoteNotificationsWithError:error]; 19 | } 20 | 21 | + (void) didReceiveRemoteNotification:(NSDictionary*)userInfo withCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { 22 | [PushNotificationAppDelegateHelper didReceiveRemoteNotificationWithUserInfo:userInfo completionHandler:completionHandler]; 23 | } 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addMessageEventListener, 3 | addTokenEventListener, 4 | addErrorListener, 5 | completeNotification, 6 | getBadgeCount, 7 | getConstants, 8 | getLaunchNotification, 9 | getPermissionStatus, 10 | registerHeadlessTask, 11 | requestPermissions, 12 | setBadgeCount, 13 | registerForToken, 14 | removeListeners, 15 | } from './apis'; 16 | 17 | export type { 18 | PushNotificationMessage, 19 | PushNotificationPermissionStatus, 20 | PushNotificationPermissions, 21 | } from './types'; 22 | 23 | const module = { 24 | addMessageEventListener, 25 | addTokenEventListener, 26 | completeNotification, 27 | getBadgeCount, 28 | getConstants, 29 | getLaunchNotification, 30 | getPermissionStatus, 31 | registerHeadlessTask, 32 | requestPermissions, 33 | setBadgeCount, 34 | registerForToken, 35 | removeListeners, 36 | addErrorListener, 37 | }; 38 | 39 | export type PushNotificationModule = typeof module; 40 | export { module }; 41 | -------------------------------------------------------------------------------- /ios/Push.mm: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface RCT_EXTERN_MODULE(Push, RCTEventEmitter) 5 | 6 | RCT_EXTERN_METHOD(supportedEvents) 7 | 8 | RCT_EXTERN_METHOD(registerForToken) 9 | 10 | RCT_EXTERN_METHOD(requestPermissions:(NSDictionary*)permissions 11 | resolve:(RCTPromiseResolveBlock)resolve 12 | reject:(RCTPromiseRejectBlock)reject) 13 | 14 | RCT_EXTERN_METHOD(getPermissionStatus:(RCTPromiseResolveBlock)resolve 15 | reject:(RCTPromiseRejectBlock)reject) 16 | 17 | RCT_EXTERN_METHOD(getLaunchNotification:(RCTPromiseResolveBlock)resolve 18 | reject:(RCTPromiseRejectBlock)reject) 19 | 20 | RCT_EXTERN_METHOD(setBadgeCount:(int)count) 21 | 22 | RCT_EXTERN_METHOD(getBadgeCount:(RCTPromiseResolveBlock)resolve 23 | reject:(RCTPromiseRejectBlock)reject) 24 | 25 | RCT_EXTERN_METHOD(completeNotification:(NSString*)completionHandlerId) 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/pushexample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.pushexample 2 | 3 | import com.facebook.react.ReactActivity 4 | import com.facebook.react.ReactActivityDelegate 5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 6 | import com.facebook.react.defaults.DefaultReactActivityDelegate 7 | 8 | class MainActivity : ReactActivity() { 9 | 10 | /** 11 | * Returns the name of the main component registered from JavaScript. This is used to schedule 12 | * rendering of the component. 13 | */ 14 | override fun getMainComponentName(): String = "PushExample" 15 | 16 | /** 17 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 18 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 19 | */ 20 | override fun createReactActivityDelegate(): ReactActivityDelegate = 21 | DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) 22 | } 23 | -------------------------------------------------------------------------------- /PushNotificationAppDelegateHelper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @objc(PushNotificationAppDelegateHelper) 4 | public class PushNotificationAppDelegateHelper: NSObject { 5 | @objc 6 | static public func didRegisterForRemoteNotificationsWithDeviceToken(_ deviceToken: Data) { 7 | PushNotificationManager 8 | .shared 9 | .didRegisterForRemoteNotificationsWithDeviceToken(deviceToken: deviceToken) 10 | } 11 | 12 | @objc 13 | static public func didFailToRegisterForRemoteNotificationsWithError(_ error: Error) { 14 | PushNotificationManager 15 | .shared 16 | .didFailToRegisterForRemoteNotificationsWithError(error: error) 17 | } 18 | 19 | @objc 20 | static public func didReceiveRemoteNotification( 21 | userInfo: [AnyHashable: Any], 22 | completionHandler: @escaping (UIBackgroundFetchResult) -> Void 23 | ) { 24 | PushNotificationManager 25 | .shared 26 | .didReceiveRemoteNotification(userInfo: userInfo, completionHandler: completionHandler) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ios/PushNotificationAppDelegateHelper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @objc(PushNotificationAppDelegateHelper) 4 | public class PushNotificationAppDelegateHelper: NSObject { 5 | @objc 6 | static public func didRegisterForRemoteNotificationsWithDeviceToken(_ deviceToken: Data) { 7 | PushNotificationManager 8 | .shared 9 | .didRegisterForRemoteNotificationsWithDeviceToken(deviceToken: deviceToken) 10 | } 11 | 12 | @objc 13 | static public func didFailToRegisterForRemoteNotificationsWithError(_ error: Error) { 14 | PushNotificationManager 15 | .shared 16 | .didFailToRegisterForRemoteNotificationsWithError(error: error) 17 | } 18 | 19 | @objc 20 | static public func didReceiveRemoteNotification( 21 | userInfo: [AnyHashable: Any], 22 | completionHandler: @escaping (UIBackgroundFetchResult) -> Void 23 | ) { 24 | PushNotificationManager 25 | .shared 26 | .didReceiveRemoteNotification(userInfo: userInfo, completionHandler: completionHandler) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .classpath 35 | .cxx 36 | .gradle 37 | .idea 38 | .project 39 | .settings 40 | local.properties 41 | android.iml 42 | 43 | # Cocoapods 44 | # 45 | example/ios/Pods 46 | 47 | # Ruby 48 | example/vendor/ 49 | 50 | # node.js 51 | # 52 | node_modules/ 53 | npm-debug.log 54 | yarn-debug.log 55 | yarn-error.log 56 | 57 | # BUCK 58 | buck-out/ 59 | \.buckd/ 60 | android/app/libs 61 | android/keystores/debug.keystore 62 | 63 | # Yarn 64 | .yarn/* 65 | !.yarn/patches 66 | !.yarn/plugins 67 | !.yarn/releases 68 | !.yarn/sdks 69 | !.yarn/versions 70 | 71 | # Expo 72 | .expo/ 73 | 74 | # Turborepo 75 | .turbo/ 76 | 77 | # generated by bob 78 | lib/ 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Gary Tokman 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /example/ios/PushExample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifestNew.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 15 | 18 | 21 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@candlefinance/push-example", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "start": "react-native start", 9 | "build:android": "cd android && ./gradlew assembleDebug --no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a", 10 | "push": "xcrun simctl push booted com.pushexample2.app payload.json", 11 | "build:ios": "cd ios && xcodebuild -workspace PushExample.xcworkspace -scheme PushExample -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO" 12 | }, 13 | "dependencies": { 14 | "react": "18.2.0", 15 | "react-native": "0.73.4" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.20.0", 19 | "@babel/preset-env": "^7.20.0", 20 | "@babel/runtime": "^7.20.0", 21 | "@react-native/babel-preset": "0.73.21", 22 | "@react-native/metro-config": "0.73.5", 23 | "@react-native/typescript-config": "0.73.1", 24 | "babel-plugin-module-resolver": "^5.0.0", 25 | "pod-install": "^0.1.0" 26 | }, 27 | "engines": { 28 | "node": ">=18" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); 2 | const path = require('path'); 3 | const escape = require('escape-string-regexp'); 4 | const exclusionList = require('metro-config/src/defaults/exclusionList'); 5 | const pak = require('../package.json'); 6 | 7 | const root = path.resolve(__dirname, '..'); 8 | const modules = Object.keys({ ...pak.peerDependencies }); 9 | 10 | /** 11 | * Metro configuration 12 | * https://facebook.github.io/metro/docs/configuration 13 | * 14 | * @type {import('metro-config').MetroConfig} 15 | */ 16 | const config = { 17 | watchFolders: [root], 18 | 19 | // We need to make sure that only one version is loaded for peerDependencies 20 | // So we block them at the root, and alias them to the versions in example's node_modules 21 | resolver: { 22 | blacklistRE: exclusionList( 23 | modules.map( 24 | (m) => 25 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) 26 | ) 27 | ), 28 | 29 | extraNodeModules: modules.reduce((acc, name) => { 30 | acc[name] = path.join(__dirname, 'node_modules', name); 31 | return acc; 32 | }, {}), 33 | }, 34 | 35 | transformer: { 36 | getTransformOptions: async () => ({ 37 | transform: { 38 | experimentalImportSupport: false, 39 | inlineRequires: true, 40 | }, 41 | }), 42 | }, 43 | }; 44 | 45 | module.exports = mergeConfig(getDefaultConfig(__dirname), config); 46 | -------------------------------------------------------------------------------- /candlefinance-push.podspec: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 4 | folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' 5 | 6 | Pod::Spec.new do |s| 7 | s.name = "candlefinance-push" 8 | s.version = package["version"] 9 | s.summary = package["description"] 10 | s.homepage = package["homepage"] 11 | s.license = package["license"] 12 | s.authors = package["author"] 13 | s.swift_version = "5.0" 14 | 15 | s.platforms = { :ios => "15.0" } 16 | s.source = { :git => "https://github.com/candlefinance/push.git", :tag => "#{s.version}" } 17 | 18 | s.source_files = "ios/**/*.{h,m,mm,swift}" 19 | 20 | # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. 21 | # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79. 22 | if respond_to?(:install_modules_dependencies, true) 23 | install_modules_dependencies(s) 24 | else 25 | s.dependency "React-Core" 26 | 27 | # Don't install the dependencies when we run `pod install` in the old architecture. 28 | if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then 29 | s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" 30 | s.pod_target_xcconfig = { 31 | "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", 32 | "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", 33 | "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" 34 | } 35 | s.dependency "React-Codegen" 36 | s.dependency "RCT-Folly" 37 | s.dependency "RCTRequired" 38 | s.dependency "RCTTypeSafety" 39 | s.dependency "ReactCommon/turbomodule/core" 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /android/src/main/java/com/candlefinance/push/RNEventEmitter.kt: -------------------------------------------------------------------------------- 1 | package com.candlefinance.push 2 | 3 | import android.util.Log 4 | import com.facebook.react.bridge.ReactApplicationContext 5 | import com.facebook.react.bridge.WritableMap 6 | import com.facebook.react.modules.core.DeviceEventManagerModule 7 | 8 | enum class PushNotificationEventType(val value: String) { 9 | FOREGROUND_MESSAGE_RECEIVED("ForegroundMessageReceived"), 10 | LAUNCH_NOTIFICATION_OPENED("LaunchNotificationOpened"), 11 | NOTIFICATION_OPENED("NotificationOpened"), 12 | BACKGROUND_MESSAGE_RECEIVED("BackgroundMessageReceived"), 13 | TOKEN_RECEIVED("TokenReceived"), 14 | FAILED_TO_REGISTER("FailedToRegister") 15 | } 16 | 17 | class PushNotificationEvent(val type: PushNotificationEventType, val params: WritableMap?) 18 | 19 | object PushNotificationEventManager { 20 | private lateinit var reactContext: ReactApplicationContext 21 | private var isInitialized: Boolean = false 22 | private val eventQueue: MutableList = mutableListOf() 23 | 24 | fun init(reactContext: ReactApplicationContext) { 25 | this.reactContext = reactContext 26 | isInitialized = true 27 | flushEventQueue() 28 | } 29 | 30 | fun sendEvent(type: PushNotificationEventType, params: WritableMap?) { 31 | if (!isInitialized) { 32 | eventQueue.add(PushNotificationEvent(type, params)) 33 | } else { 34 | Log.d("PushNotificationEventManager", "Sending event: $type") 35 | sendJSEvent(type, params) 36 | } 37 | } 38 | 39 | private fun sendJSEvent(type: PushNotificationEventType, params: WritableMap?) { 40 | reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) 41 | ?.emit(type.value, params) 42 | } 43 | 44 | private fun flushEventQueue() { 45 | eventQueue.forEach { 46 | sendJSEvent(it.type, it.params) 47 | } 48 | eventQueue.clear() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /example/ios/PushExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | PushExample 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | NSAllowsLocalNetworking 32 | 33 | 34 | NSLocationWhenInUseUsageDescription 35 | 36 | UIBackgroundModes 37 | 38 | fetch 39 | processing 40 | remote-notification 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UIViewControllerBasedStatusBarAppearance 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/pushexample/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.pushexample 2 | 3 | import android.app.Application 4 | import com.facebook.react.PackageList 5 | import com.facebook.react.ReactApplication 6 | import com.facebook.react.ReactHost 7 | import com.facebook.react.ReactNativeHost 8 | import com.facebook.react.ReactPackage 9 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load 10 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost 11 | import com.facebook.react.defaults.DefaultReactNativeHost 12 | import com.facebook.react.flipper.ReactNativeFlipper 13 | import com.facebook.soloader.SoLoader 14 | 15 | class MainApplication : Application(), ReactApplication { 16 | 17 | override val reactNativeHost: ReactNativeHost = 18 | object : DefaultReactNativeHost(this) { 19 | override fun getPackages(): List = 20 | PackageList(this).packages.apply { 21 | // Packages that cannot be autolinked yet can be added manually here, for example: 22 | // add(MyReactNativePackage()) 23 | } 24 | 25 | override fun getJSMainModuleName(): String = "index" 26 | 27 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 28 | 29 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 30 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 31 | } 32 | 33 | override val reactHost: ReactHost 34 | get() = getDefaultReactHost(this.applicationContext, reactNativeHost) 35 | 36 | override fun onCreate() { 37 | super.onCreate() 38 | SoLoader.init(this, false) 39 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 40 | // If you opted-in for the New Architecture, we load the native entry point for this app. 41 | load() 42 | } 43 | ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Use this property to specify which architecture you want to build. 28 | # You can also override it from the CLI using 29 | # ./gradlew -PreactNativeArchitectures=x86_64 30 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 31 | 32 | # Use this property to enable support to the new architecture. 33 | # This will allow you to use TurboModules and the Fabric render in 34 | # your application. You should enable this flag either if you want 35 | # to write custom TurboModules/Fabric components OR use libraries that 36 | # are providing them. 37 | newArchEnabled=false 38 | 39 | # Use this property to enable or disable the Hermes JS engine. 40 | # If set to false, you will be using JSC instead. 41 | hermesEnabled=true 42 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Resolve react_native_pods.rb with node to allow for hoisting 2 | require Pod::Executable.execute_command('node', ['-p', 3 | 'require.resolve( 4 | "react-native/scripts/react_native_pods.rb", 5 | {paths: [process.argv[1]]}, 6 | )', __dir__]).strip 7 | 8 | platform :ios, '15.0' 9 | prepare_react_native_project! 10 | 11 | # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. 12 | # because `react-native-flipper` depends on (FlipperKit,...) that will be excluded 13 | # 14 | # To fix this you can also exclude `react-native-flipper` using a `react-native.config.js` 15 | # ```js 16 | # module.exports = { 17 | # dependencies: { 18 | # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), 19 | # ``` 20 | flipper_config = FlipperConfiguration.disabled 21 | 22 | linkage = ENV['USE_FRAMEWORKS'] 23 | if linkage != nil 24 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green 25 | use_frameworks! :linkage => linkage.to_sym 26 | end 27 | 28 | target 'PushExample' do 29 | config = use_native_modules! 30 | use_modular_headers! 31 | 32 | use_react_native!( 33 | :path => config[:reactNativePath], 34 | :hermes_enabled => false, 35 | :fabric_enabled => false, 36 | # Enables Flipper. 37 | # 38 | # Note that if you have use_frameworks! enabled, Flipper will not work and 39 | # you should disable the next line. 40 | :flipper_configuration => flipper_config, 41 | # An absolute path to your application root. 42 | :app_path => "#{Pod::Config.instance.installation_root}/.." 43 | ) 44 | 45 | target 'PushExampleTests' do 46 | inherit! :complete 47 | # Pods for testing 48 | end 49 | 50 | post_install do |installer| 51 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 52 | react_native_post_install( 53 | installer, 54 | config[:reactNativePath], 55 | :mac_catalyst_enabled => false 56 | ) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /example/ios/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PushExample 4 | // 5 | // Created by Gary Tokman on 2/11/24. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import React 11 | import candlefinance_push // 0 12 | import NotificationCenter // 1 13 | 14 | @UIApplicationMain 15 | class AppDelegate: RCTAppDelegate { 16 | var isDarkMode: Bool { 17 | return UITraitCollection.current.userInterfaceStyle == .dark 18 | } 19 | 20 | override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 21 | self.moduleName = "PushExample" 22 | UNUserNotificationCenter.current().delegate = self // 3 23 | let result = super.application(application, didFinishLaunchingWithOptions: launchOptions) 24 | 25 | return result 26 | } 27 | 28 | override func sourceURL(for bridge: RCTBridge!) -> URL! { 29 | //#if DEBUG 30 | print("DEBUG") 31 | return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") 32 | //#else 33 | // print("PROD") 34 | // return Bundle.main.url(forResource: "main", withExtension: "jsbundle") 35 | //#endif 36 | } 37 | 38 | } 39 | 40 | // 4 41 | extension AppDelegate: UNUserNotificationCenterDelegate { 42 | 43 | override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { 44 | PushNotificationAppDelegateHelper.didRegisterForRemoteNotificationsWithDeviceToken(deviceToken) 45 | } 46 | 47 | override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) { 48 | PushNotificationAppDelegateHelper.didFailToRegisterForRemoteNotificationsWithError(error) 49 | } 50 | 51 | override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { 52 | PushNotificationAppDelegateHelper.didReceiveRemoteNotification(userInfo: userInfo, completionHandler: completionHandler) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Status.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UserNotifications 3 | 4 | #if canImport(WatchKit) 5 | import WatchKit 6 | #elseif canImport(UIKit) 7 | import UIKit 8 | typealias Application = UIApplication 9 | #elseif canImport(AppKit) 10 | import AppKit 11 | typealias Application = NSApplication 12 | #endif 13 | 14 | @available(iOSApplicationExtension, unavailable) 15 | @available(watchOSApplicationExtension, unavailable) 16 | @available(tvOSApplicationExtension, unavailable) 17 | @available(macCatalystApplicationExtension, unavailable) 18 | @available(OSXApplicationExtension, unavailable) 19 | /// Provides convenience methods for requesting and checking notifications permissions. 20 | public class AUNotificationPermissions { 21 | 22 | /// Check if notifications are allowed 23 | public static var allowed: Bool { 24 | get async { 25 | await status == .authorized ? true : false 26 | } 27 | } 28 | 29 | /// Check the notification permission status 30 | public static var status: UNAuthorizationStatus { 31 | get async { 32 | await withCheckedContinuation { continuation in 33 | UNUserNotificationCenter.current().getNotificationSettings { settings in 34 | continuation.resume(returning: settings.authorizationStatus) 35 | } 36 | } 37 | } 38 | } 39 | 40 | /// Request notification permissions 41 | /// - Parameter options: Requested notification options 42 | @discardableResult 43 | public static func request(_ options: UNAuthorizationOptions? = nil) async throws -> Bool { 44 | let options = options ?? [.badge, .alert, .sound] 45 | let notificationsAllowed = try await UNUserNotificationCenter.current().requestAuthorization( 46 | options: options 47 | ) 48 | 49 | return notificationsAllowed 50 | } 51 | 52 | /// Register device with APNs 53 | public static func registerForRemoteNotifications() async { 54 | await MainActor.run { 55 | #if canImport(WatchKit) 56 | WKExtension.shared().registerForRemoteNotifications() 57 | #else 58 | Application.shared.registerForRemoteNotifications() 59 | #endif 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ios/Status.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UserNotifications 3 | 4 | #if canImport(WatchKit) 5 | import WatchKit 6 | #elseif canImport(UIKit) 7 | import UIKit 8 | typealias Application = UIApplication 9 | #elseif canImport(AppKit) 10 | import AppKit 11 | typealias Application = NSApplication 12 | #endif 13 | 14 | @available(iOSApplicationExtension, unavailable) 15 | @available(watchOSApplicationExtension, unavailable) 16 | @available(tvOSApplicationExtension, unavailable) 17 | @available(macCatalystApplicationExtension, unavailable) 18 | @available(OSXApplicationExtension, unavailable) 19 | /// Provides convenience methods for requesting and checking notifications permissions. 20 | public class AUNotificationPermissions { 21 | 22 | /// Check if notifications are allowed 23 | public static var allowed: Bool { 24 | get async { 25 | await status == .authorized ? true : false 26 | } 27 | } 28 | 29 | /// Check the notification permission status 30 | public static var status: UNAuthorizationStatus { 31 | get async { 32 | await withCheckedContinuation { continuation in 33 | UNUserNotificationCenter.current().getNotificationSettings { settings in 34 | continuation.resume(returning: settings.authorizationStatus) 35 | } 36 | } 37 | } 38 | } 39 | 40 | /// Request notification permissions 41 | /// - Parameter options: Requested notification options 42 | @discardableResult 43 | public static func request(_ options: UNAuthorizationOptions? = nil) async throws -> Bool { 44 | let options = options ?? [.badge, .alert, .sound] 45 | let notificationsAllowed = try await UNUserNotificationCenter.current().requestAuthorization( 46 | options: options 47 | ) 48 | 49 | return notificationsAllowed 50 | } 51 | 52 | /// Register device with APNs 53 | public static func registerForRemoteNotifications() async { 54 | await MainActor.run { 55 | #if canImport(WatchKit) 56 | WKExtension.shared().registerForRemoteNotifications() 57 | #else 58 | Application.shared.registerForRemoteNotifications() 59 | #endif 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /example/ios/PushExampleTests/PushExampleTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface PushExampleTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation PushExampleTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction( 38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 39 | if (level >= RCTLogLevelError) { 40 | redboxError = message; 41 | } 42 | }); 43 | #endif 44 | 45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 48 | 49 | foundElement = [self findSubviewInView:vc.view 50 | matching:^BOOL(UIView *view) { 51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 52 | return YES; 53 | } 54 | return NO; 55 | }]; 56 | } 57 | 58 | #ifdef DEBUG 59 | RCTSetLogFunction(RCTDefaultLogFunction); 60 | #endif 61 | 62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /src/utils/normalizeNativeMessage.ts: -------------------------------------------------------------------------------- 1 | function isEmpty(obj: Record): boolean { 2 | for (let key in obj) { 3 | if (obj.hasOwnProperty(key)) return false; 4 | } 5 | return true; 6 | } 7 | 8 | import type { 9 | ApnsMessage, 10 | FcmMessage, 11 | NativeMessage, 12 | NormalizedValues, 13 | PushNotificationMessage, 14 | } from '../types'; 15 | 16 | /** 17 | * @internal 18 | */ 19 | export const normalizeNativeMessage = ( 20 | nativeMessage?: NativeMessage 21 | ): PushNotificationMessage | null => { 22 | let normalized: NormalizedValues; 23 | if (isApnsMessage(nativeMessage)) { 24 | normalized = normalizeApnsMessage(nativeMessage); 25 | } else if (isFcmMessage(nativeMessage)) { 26 | normalized = normalizeFcmMessage(nativeMessage); 27 | } else { 28 | return null; 29 | } 30 | const { body, imageUrl, title, options, data, custom } = normalized; 31 | 32 | return { 33 | body, 34 | data, 35 | imageUrl, 36 | title, 37 | custom, 38 | ...options, 39 | }; 40 | }; 41 | 42 | const normalizeApnsMessage = (apnsMessage: ApnsMessage): NormalizedValues => { 43 | const { aps, data, custom } = apnsMessage; 44 | const { body, title } = aps.alert ?? {}; 45 | const options = getApnsOptions(apnsMessage); 46 | 47 | return { body, title, options, data, custom: custom }; 48 | }; 49 | 50 | const normalizeFcmMessage = (fcmMessage: FcmMessage): NormalizedValues => { 51 | const { body, imageUrl, rawData: data, title, subtitle, custom } = fcmMessage; 52 | const options = getFcmOptions(fcmMessage); 53 | 54 | return { body, imageUrl, title, options, data, custom, subtitle }; 55 | }; 56 | 57 | const getApnsOptions = ({ 58 | aps, 59 | }: ApnsMessage): Pick => { 60 | const { subtitle } = aps.alert ?? {}; 61 | const apnsOptions = { ...(subtitle && { subtitle }) }; 62 | 63 | return { ...(!isEmpty(apnsOptions) && { apnsOptions }) }; 64 | }; 65 | 66 | const getFcmOptions = ({ 67 | channelId = '', 68 | messageId = '', 69 | senderId = '', 70 | sendTime = new Date().getTime(), 71 | }: FcmMessage): Pick => { 72 | const fcmOptions = { 73 | channelId, 74 | messageId, 75 | senderId, 76 | sendTime: new Date(sendTime), 77 | }; 78 | 79 | return { ...(!isEmpty(fcmOptions) && { fcmOptions }) }; 80 | }; 81 | 82 | const isApnsMessage = ( 83 | nativeMessage?: NativeMessage 84 | ): nativeMessage is ApnsMessage => !!nativeMessage?.aps; 85 | 86 | const isFcmMessage = ( 87 | nativeMessage?: NativeMessage 88 | ): nativeMessage is FcmMessage => !!nativeMessage?.rawData; 89 | -------------------------------------------------------------------------------- /src/types/native.ts: -------------------------------------------------------------------------------- 1 | import type { NativeModule } from 'react-native'; 2 | 3 | import type { 4 | PushNotificationMessage, 5 | PushNotificationPermissions, 6 | } from './module'; 7 | 8 | export interface PushNotificationNativeModule extends NativeModule { 9 | completeNotification?(completionHandlerId: string): void; 10 | getConstants(): { 11 | NativeEvent: { 12 | BACKGROUND_MESSAGE_RECEIVED: string; 13 | FOREGROUND_MESSAGE_RECEIVED: string; 14 | LAUNCH_NOTIFICATION_OPENED: string; 15 | NOTIFICATION_OPENED: string; 16 | TOKEN_RECEIVED: string; 17 | FAILED_TO_REGISTER: string; 18 | }; 19 | NativeHeadlessTaskKey?: string; 20 | }; 21 | getLaunchNotification(): Promise; 22 | getBadgeCount?(): Promise; 23 | setBadgeCount?(count: number): void; 24 | getPermissionStatus(): Promise; 25 | requestPermissions( 26 | permissions: PushNotificationPermissions 27 | ): Promise; 28 | registerForToken: () => void; 29 | } 30 | 31 | export interface NativeAction { 32 | deeplink?: string; 33 | url?: string; 34 | } 35 | 36 | export type NativeMessage = (ApnsMessage | FcmMessage) & { 37 | token?: never; 38 | }; 39 | 40 | export type NativePermissionStatus = 41 | | AndroidPermissionStatus 42 | | IosPermissionStatus; 43 | 44 | export interface NormalizedValues { 45 | body?: string; 46 | imageUrl?: string; 47 | title?: string; 48 | subtitle?: string; 49 | options?: Pick; 50 | data?: Record; 51 | custom?: any; 52 | } 53 | 54 | // iOS 55 | export interface ApnsMessage { 56 | aps: { 57 | 'alert'?: { 58 | title?: string; 59 | body?: string; 60 | subtitle?: string; 61 | }; 62 | 'sound'?: string; 63 | 'badge'?: number; 64 | 'content-available'?: number; 65 | 'category'?: string; 66 | }; 67 | custom?: any; 68 | rawData?: never; 69 | completionHandlerId?: string; 70 | data?: Record; 71 | } 72 | 73 | export type IosPermissionStatus = 'NotDetermined' | 'Authorized' | 'Denied'; 74 | 75 | // Android 76 | export interface FcmMessage { 77 | action?: NativeAction; 78 | aps?: never; 79 | body?: string; 80 | imageUrl?: string; 81 | rawData?: Record; 82 | title?: string; 83 | subtitle?: string; 84 | channelId?: string; 85 | messageId?: string; 86 | senderId?: string; 87 | sendTime?: number; 88 | completionHandlerId?: never; 89 | custom?: any; 90 | } 91 | 92 | export type AndroidPermissionStatus = 93 | | 'ShouldRequest' 94 | | 'ShouldExplainThenRequest' 95 | | 'Granted' 96 | | 'Denied'; 97 | 98 | export interface TokenPayload { 99 | token: string; 100 | } 101 | -------------------------------------------------------------------------------- /example/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.6) 5 | rexml 6 | activesupport (7.0.8) 7 | concurrent-ruby (~> 1.0, >= 1.0.2) 8 | i18n (>= 1.6, < 2) 9 | minitest (>= 5.1) 10 | tzinfo (~> 2.0) 11 | addressable (2.8.6) 12 | public_suffix (>= 2.0.2, < 6.0) 13 | algoliasearch (1.27.5) 14 | httpclient (~> 2.8, >= 2.8.3) 15 | json (>= 1.5.1) 16 | atomos (0.1.3) 17 | claide (1.1.0) 18 | cocoapods (1.14.3) 19 | addressable (~> 2.8) 20 | claide (>= 1.0.2, < 2.0) 21 | cocoapods-core (= 1.14.3) 22 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 23 | cocoapods-downloader (>= 2.1, < 3.0) 24 | cocoapods-plugins (>= 1.0.0, < 2.0) 25 | cocoapods-search (>= 1.0.0, < 2.0) 26 | cocoapods-trunk (>= 1.6.0, < 2.0) 27 | cocoapods-try (>= 1.1.0, < 2.0) 28 | colored2 (~> 3.1) 29 | escape (~> 0.0.4) 30 | fourflusher (>= 2.3.0, < 3.0) 31 | gh_inspector (~> 1.0) 32 | molinillo (~> 0.8.0) 33 | nap (~> 1.0) 34 | ruby-macho (>= 2.3.0, < 3.0) 35 | xcodeproj (>= 1.23.0, < 2.0) 36 | cocoapods-core (1.14.3) 37 | activesupport (>= 5.0, < 8) 38 | addressable (~> 2.8) 39 | algoliasearch (~> 1.0) 40 | concurrent-ruby (~> 1.1) 41 | fuzzy_match (~> 2.0.4) 42 | nap (~> 1.0) 43 | netrc (~> 0.11) 44 | public_suffix (~> 4.0) 45 | typhoeus (~> 1.0) 46 | cocoapods-deintegrate (1.0.5) 47 | cocoapods-downloader (2.1) 48 | cocoapods-plugins (1.0.0) 49 | nap 50 | cocoapods-search (1.0.1) 51 | cocoapods-trunk (1.6.0) 52 | nap (>= 0.8, < 2.0) 53 | netrc (~> 0.11) 54 | cocoapods-try (1.2.0) 55 | colored2 (3.1.2) 56 | concurrent-ruby (1.2.3) 57 | escape (0.0.4) 58 | ethon (0.16.0) 59 | ffi (>= 1.15.0) 60 | ffi (1.16.3) 61 | fourflusher (2.3.1) 62 | fuzzy_match (2.0.4) 63 | gh_inspector (1.1.3) 64 | httpclient (2.8.3) 65 | i18n (1.14.1) 66 | concurrent-ruby (~> 1.0) 67 | json (2.7.1) 68 | minitest (5.22.2) 69 | molinillo (0.8.0) 70 | nanaimo (0.3.0) 71 | nap (1.1.0) 72 | netrc (0.11.0) 73 | public_suffix (4.0.7) 74 | rexml (3.2.6) 75 | ruby-macho (2.5.1) 76 | typhoeus (1.4.1) 77 | ethon (>= 0.9.0) 78 | tzinfo (2.0.6) 79 | concurrent-ruby (~> 1.0) 80 | xcodeproj (1.24.0) 81 | CFPropertyList (>= 2.3.3, < 4.0) 82 | atomos (~> 0.1.3) 83 | claide (>= 1.0.2, < 2.0) 84 | colored2 (~> 3.1) 85 | nanaimo (~> 0.3.0) 86 | rexml (~> 3.2.4) 87 | 88 | PLATFORMS 89 | ruby 90 | 91 | DEPENDENCIES 92 | activesupport (>= 6.1.7.5, < 7.1.0) 93 | cocoapods (>= 1.13, < 1.15) 94 | 95 | RUBY VERSION 96 | ruby 2.7.5p203 97 | 98 | BUNDLED WITH 99 | 2.3.9 100 | -------------------------------------------------------------------------------- /example/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | // Buildscript is evaluated before everything else so we can't use getExtOrDefault 3 | def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["Push_kotlinVersion"] 4 | 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | classpath "com.android.tools.build:gradle:7.2.1" 12 | // noinspection DifferentKotlinGradleVersion 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | def isNewArchitectureEnabled() { 18 | return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" 19 | } 20 | 21 | apply plugin: "com.android.library" 22 | apply plugin: "kotlin-android" 23 | apply plugin: 'kotlin-parcelize' 24 | 25 | if (isNewArchitectureEnabled()) { 26 | apply plugin: "com.facebook.react" 27 | } 28 | 29 | def getExtOrDefault(name) { 30 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["Push_" + name] 31 | } 32 | 33 | def getExtOrIntegerDefault(name) { 34 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["Push_" + name]).toInteger() 35 | } 36 | 37 | def supportsNamespace() { 38 | def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.') 39 | def major = parsed[0].toInteger() 40 | def minor = parsed[1].toInteger() 41 | 42 | // Namespace support was added in 7.3.0 43 | return (major == 7 && minor >= 3) || major >= 8 44 | } 45 | 46 | android { 47 | if (supportsNamespace()) { 48 | namespace "com.candlefinance.push" 49 | 50 | sourceSets { 51 | main { 52 | manifest.srcFile "src/main/AndroidManifestNew.xml" 53 | } 54 | } 55 | } 56 | 57 | compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") 58 | 59 | defaultConfig { 60 | minSdkVersion getExtOrIntegerDefault("minSdkVersion") 61 | targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") 62 | 63 | } 64 | 65 | buildTypes { 66 | release { 67 | minifyEnabled false 68 | } 69 | } 70 | 71 | lintOptions { 72 | disable "GradleCompatible" 73 | } 74 | 75 | compileOptions { 76 | sourceCompatibility JavaVersion.VERSION_1_8 77 | targetCompatibility JavaVersion.VERSION_1_8 78 | } 79 | } 80 | 81 | repositories { 82 | mavenCentral() 83 | google() 84 | } 85 | 86 | def kotlin_version = getExtOrDefault("kotlinVersion") 87 | 88 | dependencies { 89 | // For < 0.71, this will be from the local maven repo 90 | // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin 91 | //noinspection GradleDynamicVersion 92 | implementation "com.facebook.react:react-native:+" 93 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 94 | implementation(platform("com.google.firebase:firebase-bom:32.8.1")) 95 | implementation "com.google.firebase:firebase-messaging" 96 | implementation 'androidx.core:core-ktx:1.13.0' 97 | implementation 'androidx.lifecycle:lifecycle-process:2.7.0' 98 | } 99 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { 100 | kotlinOptions { 101 | languageVersion = "1.9" 102 | } 103 | } 104 | 105 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). 2 | 3 | # Getting Started 4 | 5 | >**Note**: Make sure you have completed the [React Native - Environment Setup](https://reactnative.dev/docs/environment-setup) instructions till "Creating a new application" step, before proceeding. 6 | 7 | ## Step 1: Start the Metro Server 8 | 9 | First, you will need to start **Metro**, the JavaScript _bundler_ that ships _with_ React Native. 10 | 11 | To start Metro, run the following command from the _root_ of your React Native project: 12 | 13 | ```bash 14 | # using npm 15 | npm start 16 | 17 | # OR using Yarn 18 | yarn start 19 | ``` 20 | 21 | ## Step 2: Start your Application 22 | 23 | Let Metro Bundler run in its _own_ terminal. Open a _new_ terminal from the _root_ of your React Native project. Run the following command to start your _Android_ or _iOS_ app: 24 | 25 | ### For Android 26 | 27 | ```bash 28 | # using npm 29 | npm run android 30 | 31 | # OR using Yarn 32 | yarn android 33 | ``` 34 | 35 | ### For iOS 36 | 37 | ```bash 38 | # using npm 39 | npm run ios 40 | 41 | # OR using Yarn 42 | yarn ios 43 | ``` 44 | 45 | If everything is set up _correctly_, you should see your new app running in your _Android Emulator_ or _iOS Simulator_ shortly provided you have set up your emulator/simulator correctly. 46 | 47 | This is one way to run your app — you can also run it directly from within Android Studio and Xcode respectively. 48 | 49 | ## Step 3: Modifying your App 50 | 51 | Now that you have successfully run the app, let's modify it. 52 | 53 | 1. Open `App.tsx` in your text editor of choice and edit some lines. 54 | 2. For **Android**: Press the R key twice or select **"Reload"** from the **Developer Menu** (Ctrl + M (on Window and Linux) or Cmd ⌘ + M (on macOS)) to see your changes! 55 | 56 | For **iOS**: Hit Cmd ⌘ + R in your iOS Simulator to reload the app and see your changes! 57 | 58 | ## Congratulations! :tada: 59 | 60 | You've successfully run and modified your React Native App. :partying_face: 61 | 62 | ### Now what? 63 | 64 | - If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps). 65 | - If you're curious to learn more about React Native, check out the [Introduction to React Native](https://reactnative.dev/docs/getting-started). 66 | 67 | # Troubleshooting 68 | 69 | If you can't get this to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page. 70 | 71 | # Learn More 72 | 73 | To learn more about React Native, take a look at the following resources: 74 | 75 | - [React Native Website](https://reactnative.dev) - learn more about React Native. 76 | - [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment. 77 | - [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**. 78 | - [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts. 79 | - [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native. 80 | -------------------------------------------------------------------------------- /example/ios/PushExample.xcodeproj/xcshareddata/xcschemes/PushExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import type { PushNotificationPermissionStatus } from '@candlefinance/push'; 4 | import { module as Push } from '@candlefinance/push'; 5 | import { Button, StyleSheet, Text, View } from 'react-native'; 6 | 7 | Push.registerHeadlessTask(async (message) => { 8 | console.log('Headless Task', message); 9 | }); 10 | 11 | export default function App() { 12 | const [permissionStatus, setPermissionStatus] = React.useState< 13 | PushNotificationPermissionStatus | undefined 14 | >(undefined); 15 | const [isGranted, setIsGranted] = React.useState(false); 16 | 17 | React.useEffect(() => { 18 | const { NativeEvent, NativeHeadlessTaskKey } = Push.getConstants(); 19 | console.log(NativeEvent, NativeHeadlessTaskKey); 20 | Push.addTokenEventListener(NativeEvent.TOKEN_RECEIVED, (token) => { 21 | console.log('TOKEN_RECEIVED:', token); 22 | }); 23 | Push.addMessageEventListener( 24 | NativeEvent.BACKGROUND_MESSAGE_RECEIVED, 25 | (message, id) => { 26 | console.log('BACKGROUND_MESSAGE_RECEIVED:', message); 27 | if (id !== undefined) { 28 | console.log('Completing notification:', id); 29 | Push.completeNotification(id); 30 | } 31 | } 32 | ); 33 | Push.addErrorListener(NativeEvent.FAILED_TO_REGISTER, (message) => { 34 | console.log('FAILED_TO_REGISTER:', message); 35 | }); 36 | Push.addMessageEventListener(NativeEvent.NOTIFICATION_OPENED, (message) => { 37 | console.log('NOTIFICATION_OPENED:', message); 38 | }); 39 | Push.addMessageEventListener( 40 | NativeEvent.FOREGROUND_MESSAGE_RECEIVED, 41 | (message) => { 42 | console.log('FOREGROUND_MESSAGE_RECEIVED:', message); 43 | } 44 | ); 45 | Push.addMessageEventListener( 46 | NativeEvent.LAUNCH_NOTIFICATION_OPENED, 47 | (message) => { 48 | console.log('LAUNCH_NOTIFICATION_OPENED:', message); 49 | } 50 | ); 51 | return () => { 52 | Push.removeListeners(NativeEvent.TOKEN_RECEIVED); 53 | Push.removeListeners(NativeEvent.BACKGROUND_MESSAGE_RECEIVED); 54 | Push.removeListeners(NativeEvent.NOTIFICATION_OPENED); 55 | Push.removeListeners(NativeEvent.FOREGROUND_MESSAGE_RECEIVED); 56 | Push.removeListeners(NativeEvent.LAUNCH_NOTIFICATION_OPENED); 57 | }; 58 | }, [isGranted]); 59 | 60 | return ( 61 | 62 | Authorization Status: {permissionStatus} 63 | isGranted: {isGranted} 64 |