├── .editorconfig ├── .gitattributes ├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ └── ci.yml ├── .gitignore ├── .nvmrc ├── .watchmanconfig ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-interactive-tools.cjs │ │ └── plugin-workspace-tools.cjs └── releases │ └── yarn-3.6.1.cjs ├── .yarnrc.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── PushHelper.swift ├── PushNotification.h ├── PushNotification.m ├── PushNotificationAppDelegateHelper.swift ├── README.md ├── Status.swift ├── android ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ ├── AndroidManifestNew.xml │ ├── java │ └── com │ │ └── candlefinance │ │ └── push │ │ ├── ContextHolder.kt │ │ ├── FirebaseMessagingService.kt │ │ ├── NotificationUtils.kt │ │ ├── PushModule.kt │ │ ├── PushPackage.kt │ │ └── RNEventEmitter.kt │ └── res │ ├── drawable-anydpi-v24 │ └── ic_default_notification.xml │ ├── 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 ├── babel.config.js ├── candlefinance-push.podspec ├── example ├── .bundle │ └── config ├── .watchmanconfig ├── Gemfile ├── Gemfile.lock ├── README.md ├── android │ ├── app │ │ ├── build.gradle │ │ ├── debug.keystore │ │ ├── google-services.json │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── pushexample │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainApplication.kt │ │ │ └── res │ │ │ ├── drawable-anydpi-v24 │ │ │ └── ic_stat_name.xml │ │ │ ├── drawable │ │ │ └── rn_edit_text_material.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 │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── app.json ├── babel.config.js ├── index.js ├── ios │ ├── .xcode.env │ ├── .xcode.env.local │ ├── AppDelegate.swift │ ├── File.swift │ ├── Podfile │ ├── Podfile.lock │ ├── PushExample-Bridging-Header.h │ ├── PushExample.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── PushExample.xcscheme │ ├── PushExample.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── PushExample │ │ ├── Images.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── LaunchScreen.storyboard │ │ └── PushExample.entitlements │ └── PushExampleTests │ │ ├── Info.plist │ │ └── PushExampleTests.m ├── jest.config.js ├── metro.config.js ├── package.json ├── payload.json ├── react-native.config.js └── src │ └── App.tsx ├── ios ├── Push-Bridging-Header.h ├── Push.mm ├── Push.swift ├── PushHelper.swift ├── PushNotification.h ├── PushNotification.m ├── PushNotificationAppDelegateHelper.swift └── Status.swift ├── lefthook.yml ├── package.json ├── src ├── apis │ ├── addErrorListener.ts │ ├── addMessageEventListener.ts │ ├── addTokenEventListener.ts │ ├── completeNotification.ts │ ├── getBadgeCount.ts │ ├── getConstants.ts │ ├── getLaunchNotification.ts │ ├── getPermissionStatus.ts │ ├── index.ts │ ├── registerForToken.ts │ ├── registerHeadlessTask.ts │ ├── removeListeners.ts │ ├── requestPermissions.ts │ └── setBadgeCount.ts ├── constants.ts ├── index.ts ├── nativeModule.ts ├── types │ ├── index.ts │ ├── module.ts │ └── native.ts └── utils │ ├── index.ts │ ├── normalizeNativeMessage.ts │ └── normalizeNativePermissionStatus.ts ├── tsconfig.build.json ├── tsconfig.json ├── turbo.json └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup 18 | uses: ./.github/actions/setup 19 | 20 | - name: Lint files 21 | run: yarn lint 22 | 23 | - name: Typecheck files 24 | run: yarn typecheck 25 | 26 | test: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v3 31 | 32 | - name: Setup 33 | uses: ./.github/actions/setup 34 | 35 | - name: Run unit tests 36 | run: yarn test --maxWorkers=2 --coverage 37 | 38 | build-library: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v3 43 | 44 | - name: Setup 45 | uses: ./.github/actions/setup 46 | 47 | - name: Build package 48 | run: yarn prepare 49 | 50 | build-android: 51 | runs-on: ubuntu-latest 52 | env: 53 | TURBO_CACHE_DIR: .turbo/android 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v3 57 | 58 | - name: Setup 59 | uses: ./.github/actions/setup 60 | 61 | - name: Cache turborepo for Android 62 | uses: actions/cache@v3 63 | with: 64 | path: ${{ env.TURBO_CACHE_DIR }} 65 | key: ${{ runner.os }}-turborepo-android-${{ hashFiles('yarn.lock') }} 66 | restore-keys: | 67 | ${{ runner.os }}-turborepo-android- 68 | 69 | - name: Check turborepo cache for Android 70 | run: | 71 | TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android').cache.status") 72 | 73 | if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then 74 | echo "turbo_cache_hit=1" >> $GITHUB_ENV 75 | fi 76 | 77 | - name: Install JDK 78 | if: env.turbo_cache_hit != 1 79 | uses: actions/setup-java@v3 80 | with: 81 | distribution: 'zulu' 82 | java-version: '17' 83 | 84 | - name: Finalize Android SDK 85 | if: env.turbo_cache_hit != 1 86 | run: | 87 | /bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null" 88 | 89 | - name: Cache Gradle 90 | if: env.turbo_cache_hit != 1 91 | uses: actions/cache@v3 92 | with: 93 | path: | 94 | ~/.gradle/wrapper 95 | ~/.gradle/caches 96 | key: ${{ runner.os }}-gradle-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }} 97 | restore-keys: | 98 | ${{ runner.os }}-gradle- 99 | 100 | - name: Build example for Android 101 | run: | 102 | yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" 103 | 104 | build-ios: 105 | runs-on: macos-14 106 | env: 107 | TURBO_CACHE_DIR: .turbo/ios 108 | steps: 109 | - name: Checkout 110 | uses: actions/checkout@v3 111 | 112 | - uses: maxim-lobanov/setup-xcode@v1 113 | with: 114 | xcode-version: latest-stable 115 | 116 | - name: Setup 117 | uses: ./.github/actions/setup 118 | 119 | - name: Cache turborepo for iOS 120 | uses: actions/cache@v3 121 | with: 122 | path: ${{ env.TURBO_CACHE_DIR }} 123 | key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('yarn.lock') }} 124 | restore-keys: | 125 | ${{ runner.os }}-turborepo-ios- 126 | 127 | - name: Check turborepo cache for iOS 128 | run: | 129 | TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status") 130 | 131 | if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then 132 | echo "turbo_cache_hit=1" >> $GITHUB_ENV 133 | fi 134 | 135 | - name: Cache cocoapods 136 | if: env.turbo_cache_hit != 1 137 | id: cocoapods-cache 138 | uses: actions/cache@v3 139 | with: 140 | path: | 141 | **/ios/Pods 142 | key: ${{ runner.os }}-cocoapods-${{ hashFiles('example/ios/Podfile.lock') }} 143 | restore-keys: | 144 | ${{ runner.os }}-cocoapods- 145 | 146 | - name: Install cocoapods 147 | if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' 148 | run: | 149 | yarn pod-install example/ios 150 | env: 151 | NO_FLIPPER: 1 152 | 153 | - name: Build example for iOS 154 | run: | 155 | yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" 156 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome, no matter how large or small! 4 | 5 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). 6 | 7 | ## Development workflow 8 | 9 | This project is a monorepo managed using [Yarn workspaces](https://yarnpkg.com/features/workspaces). It contains the following packages: 10 | 11 | - The library package in the root directory. 12 | - An example app in the `example/` directory. 13 | 14 | To get started with the project, run `yarn` in the root directory to install the required dependencies for each package: 15 | 16 | ```sh 17 | yarn 18 | ``` 19 | 20 | > Since the project relies on Yarn workspaces, you cannot use [`npm`](https://github.com/npm/cli) for development. 21 | 22 | The [example app](/example/) demonstrates usage of the library. You need to run it to test any changes you make. 23 | 24 | It is configured to use the local version of the library, so any changes you make to the library's source code will be reflected in the example app. Changes to the library's JavaScript code will be reflected in the example app without a rebuild, but native code changes will require a rebuild of the example app. 25 | 26 | If you want to use Android Studio or XCode to edit the native code, you can open the `example/android` or `example/ios` directories respectively in those editors. To edit the Objective-C or Swift files, open `example/ios/PushExample.xcworkspace` in XCode and find the source files at `Pods > Development Pods > @candlefinance/push`. 27 | 28 | To edit the Java or Kotlin files, open `example/android` in Android studio and find the source files at `candlefinance-push` under `Android`. 29 | 30 | You can use various commands from the root directory to work with the project. 31 | 32 | To start the packager: 33 | 34 | ```sh 35 | yarn example start 36 | ``` 37 | 38 | To run the example app on Android: 39 | 40 | ```sh 41 | yarn example android 42 | ``` 43 | 44 | To run the example app on iOS: 45 | 46 | ```sh 47 | yarn example ios 48 | ``` 49 | 50 | Make sure your code passes TypeScript and ESLint. Run the following to verify: 51 | 52 | ```sh 53 | yarn typecheck 54 | yarn lint 55 | ``` 56 | 57 | To fix formatting errors, run the following: 58 | 59 | ```sh 60 | yarn lint --fix 61 | ``` 62 | 63 | Remember to add tests for your change if possible. Run the unit tests by: 64 | 65 | ```sh 66 | yarn test 67 | ``` 68 | 69 | ### Commit message convention 70 | 71 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 72 | 73 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 74 | - `feat`: new features, e.g. add new method to the module. 75 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 76 | - `docs`: changes into documentation, e.g. add usage example for the module.. 77 | - `test`: adding or updating tests, e.g. add integration tests using detox. 78 | - `chore`: tooling changes, e.g. change CI config. 79 | 80 | Our pre-commit hooks verify that your commit message matches this format when committing. 81 | 82 | ### Linting and tests 83 | 84 | [ESLint](https://eslint.org/), [Prettier](https://prettier.io/), [TypeScript](https://www.typescriptlang.org/) 85 | 86 | We use [TypeScript](https://www.typescriptlang.org/) for type checking, [ESLint](https://eslint.org/) with [Prettier](https://prettier.io/) for linting and formatting the code, and [Jest](https://jestjs.io/) for testing. 87 | 88 | Our pre-commit hooks verify that the linter and tests pass when committing. 89 | 90 | ### Publishing to npm 91 | 92 | We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. 93 | 94 | To publish new versions, run the following: 95 | 96 | ```sh 97 | yarn release 98 | ``` 99 | 100 | ### Scripts 101 | 102 | The `package.json` file contains various scripts for common tasks: 103 | 104 | - `yarn`: setup project by installing dependencies. 105 | - `yarn typecheck`: type-check files with TypeScript. 106 | - `yarn lint`: lint files with ESLint. 107 | - `yarn test`: run unit tests with Jest. 108 | - `yarn example start`: start the Metro server for the example app. 109 | - `yarn example android`: run the example app on Android. 110 | - `yarn example ios`: run the example app on iOS. 111 | 112 | ### Sending a pull request 113 | 114 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). 115 | 116 | When you're sending a pull request: 117 | 118 | - Prefer small pull requests focused on one change. 119 | - Verify that linters and tests are passing. 120 | - Review the documentation to make sure it looks good. 121 | - Follow the pull request template when opening a pull request. 122 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 123 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /PushHelper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | private let completionHandlerIdKey = "completionHandlerId" 4 | 5 | extension UNAuthorizationStatus { 6 | var description : String { 7 | switch self { 8 | case .notDetermined: 9 | return "NotDetermined" 10 | case .denied: 11 | return "Denied" 12 | case .authorized: 13 | return "Authorized" 14 | case .provisional: 15 | return "Provisional" 16 | case .ephemeral: 17 | return "Ephemeral" 18 | @unknown default: 19 | return "NotDetermined" 20 | } 21 | } 22 | } 23 | 24 | enum NativeEvent { 25 | case tokenReceived 26 | case notificationOpened 27 | case launchNotificationOpened 28 | case backgroundMessageReceived 29 | case foregroundMessageReceived 30 | 31 | var key: String { 32 | switch(self) { 33 | case .tokenReceived: 34 | return "TOKEN_RECEIVED" 35 | case .notificationOpened: 36 | return "NOTIFICATION_OPENED" 37 | case .launchNotificationOpened: 38 | return "LAUNCH_NOTIFICATION_OPENED" 39 | case .backgroundMessageReceived: 40 | return "BACKGROUND_MESSAGE_RECEIVED" 41 | case .foregroundMessageReceived: 42 | return "FOREGROUND_MESSAGE_RECEIVED" 43 | } 44 | } 45 | 46 | var name: String { 47 | switch(self) { 48 | case .tokenReceived: 49 | return "TokenReceived" 50 | case .notificationOpened: 51 | return "NotificationOpened" 52 | case .launchNotificationOpened: 53 | return "LaunchNotificationOpened" 54 | case .foregroundMessageReceived: 55 | return "ForegroundMessageReceived" 56 | case .backgroundMessageReceived: 57 | return "BackgroundMessageReceived" 58 | } 59 | } 60 | } 61 | 62 | struct PushEvent { 63 | var type: NativeEvent 64 | var payload: Any 65 | } 66 | 67 | class PushEventManager { 68 | static let shared = PushEventManager() 69 | 70 | private var eventQueue: [PushEvent] = [] 71 | private var sendEvent: ((PushEvent) -> Void)? 72 | 73 | func setSendEvent(sendEvent: @escaping (PushEvent) -> Void) { 74 | self.sendEvent = sendEvent 75 | flushQueuedEvents() 76 | } 77 | 78 | func sendEventToJS(_ event: PushEvent) { 79 | if let sendEvent = self.sendEvent { 80 | sendEvent(event) 81 | } else { 82 | eventQueue.append(event) 83 | } 84 | } 85 | 86 | private func flushQueuedEvents() { 87 | while (!eventQueue.isEmpty) { 88 | sendEventToJS(eventQueue.removeFirst()) 89 | } 90 | } 91 | } 92 | 93 | final class PushNotificationManager { 94 | static let shared = PushNotificationManager() 95 | 96 | private var cachedDeviceToken: String? 97 | private var launchNotification: [AnyHashable: Any]? 98 | private var remoteNotificationCompletionHandlers: [String: (UIBackgroundFetchResult) -> Void] = [:] 99 | private let sharedEventManager: PushEventManager 100 | 101 | init() { 102 | sharedEventManager = PushEventManager.shared 103 | setUpObservers() 104 | } 105 | 106 | deinit { 107 | removeObservers() 108 | } 109 | 110 | func handleLaunchOptions(launchOptions: [AnyHashable: Any]) { 111 | // 1. The host App launch is caused by a notification 112 | if let remoteNotification = launchOptions[UIApplication.LaunchOptionsKey.remoteNotification] as? [AnyHashable: Any], 113 | let application = RCTSharedApplication() { 114 | // 2. The host App is launched from terminated state to the foreground 115 | // (including transitioning to foregound), i.e. .active .inactive. 116 | // This happens under one of below conditions: 117 | // a. Remote notifications are not able to launch the host App (without `content-available: 1`) 118 | // b. Remote notifications background mode was not enabled on the host App 119 | // c. The end user disabled background refresh of the host App 120 | // 3. This notification must be tapped by an end user which is recorded as the launch notification 121 | if application.applicationState != .background { 122 | launchNotification = remoteNotification 123 | 124 | // NOTE: the notification payload will also be passed into didReceiveRemoteNotification below after 125 | // this delegate method, didFinishLaunchingWithOptions completes. 126 | // As this notification will already be recorded as the launch notification, it should not be sent as 127 | // notificationOpened event, this check is handled in didReceiveRemoteNotification. 128 | } 129 | 130 | // Otherwise the host App is launched in the background, this notification will be sent to react-native 131 | // as backgroundMessageReceived event in didReceiveRemoteNotification below. 132 | // After the host App launched in the background, didFinishLaunchingWithOptions will no longer 133 | // be fired when an end user taps a notification. 134 | // After the host App launched in the background, it runs developers' react-native code as well. 135 | } 136 | } 137 | 138 | func requestPermissions( 139 | _ permissions: [AnyHashable: Any], 140 | resolve: @escaping RCTPromiseResolveBlock, 141 | reject: @escaping RCTPromiseRejectBlock 142 | ) { 143 | if RCTRunningInAppExtension() { 144 | reject("ERROR", "requestPermissions can not be called in App Extensions", nil) 145 | return 146 | } 147 | 148 | Task { 149 | var options: UNAuthorizationOptions = [] 150 | 151 | if permissions["alert"] as? Bool == true { 152 | options.insert(.alert) 153 | } 154 | 155 | if permissions["badge"] as? Bool == true { 156 | options.insert(.badge) 157 | } 158 | 159 | if permissions["sound"] as? Bool == true { 160 | options.insert(.sound) 161 | } 162 | 163 | if permissions["criticalAlert"] as? Bool == true { 164 | options.insert(.criticalAlert) 165 | } 166 | 167 | if permissions["provisional"] as? Bool == true { 168 | options.insert(.provisional) 169 | } 170 | 171 | do { 172 | let granted = try await AUNotificationPermissions.request(options) 173 | resolve(granted) 174 | } catch { 175 | reject("ERROR", error.localizedDescription, error) 176 | } 177 | } 178 | } 179 | 180 | func getPermissionStatus( 181 | _ resolve: @escaping RCTPromiseResolveBlock, 182 | reject: @escaping RCTPromiseRejectBlock 183 | ) { 184 | Task { 185 | let status = await AUNotificationPermissions.status 186 | resolve(status.description) 187 | } 188 | } 189 | 190 | func getLaunchNotification( 191 | _ resolve: RCTPromiseResolveBlock, 192 | reject: RCTPromiseRejectBlock 193 | ) { 194 | let launchNotification = self.launchNotification 195 | self.launchNotification = nil 196 | resolve(launchNotification == nil ? NSNull() : launchNotification) 197 | } 198 | 199 | func setBadgeCount(_ count: Int) { 200 | DispatchQueue.main.async { 201 | RCTSharedApplication()?.applicationIconBadgeNumber = count 202 | } 203 | } 204 | 205 | func getBadgeCount( 206 | _ resolve: @escaping RCTPromiseResolveBlock, 207 | reject: RCTPromiseRejectBlock 208 | ) { 209 | DispatchQueue.main.async { 210 | resolve(RCTSharedApplication()?.applicationIconBadgeNumber ?? 0) 211 | } 212 | } 213 | 214 | func didRegisterForRemoteNotificationsWithDeviceToken(deviceToken: Data) { 215 | let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() 216 | 217 | // Reduce frequency of tokenReceived event emitting to RN 218 | if (cachedDeviceToken != token) { 219 | cachedDeviceToken = token 220 | sharedEventManager.sendEventToJS( 221 | PushEvent(type: NativeEvent.tokenReceived, payload: ["token": cachedDeviceToken]) 222 | ) 223 | } 224 | } 225 | 226 | func didFailToRegisterForRemoteNotificationsWithError(error: Error) { 227 | print("Register for remote notifications failed due to \(error).") 228 | } 229 | 230 | func didReceiveRemoteNotification( 231 | userInfo: [AnyHashable: Any], 232 | completionHandler: @escaping (UIBackgroundFetchResult) -> Void 233 | ) { 234 | if let application = RCTSharedApplication() { 235 | switch application.applicationState { 236 | case .background: 237 | let completionHandlerId = UUID().uuidString 238 | var userInfoCopy = userInfo 239 | 240 | remoteNotificationCompletionHandlers[completionHandlerIdKey] = completionHandler 241 | userInfoCopy[completionHandlerIdKey] = completionHandlerId 242 | 243 | sharedEventManager.sendEventToJS( 244 | PushEvent(type: NativeEvent.backgroundMessageReceived, payload: userInfoCopy) 245 | ) 246 | 247 | // Expecting remoteNotificationCompletionHandlers[completionHandlerIdKey] to be called from JS to complete 248 | // the background notification 249 | case .inactive: 250 | if let launchNotification = launchNotification { 251 | if NSDictionary(dictionary: launchNotification).isEqual(to: userInfo) { 252 | // When the last tapped notification is the same as the launch notification, 253 | // it's sent as launchNotificationOpened event, and retrievable via getLaunchNotification. 254 | PushEventManager.shared.sendEventToJS( 255 | PushEvent(type: NativeEvent.launchNotificationOpened, payload: launchNotification) 256 | ) 257 | } else { 258 | // When a launch notification is recorded in handleLaunchOptions above, 259 | // but the last tapped notification is not the recorded launch notification, the last 260 | // tapped notification will be sent to react-native as notificationOpened event. 261 | // This may happen when an end user rapidly tapped on multiple notifications. 262 | self.launchNotification = nil 263 | sharedEventManager.sendEventToJS( 264 | PushEvent(type: NativeEvent.notificationOpened, payload: userInfo) 265 | ) 266 | } 267 | } else { 268 | // When there is no launch notification recorded, the last tapped notification 269 | // will be sent to react-native as notificationOpened event. 270 | sharedEventManager.sendEventToJS( 271 | PushEvent(type: NativeEvent.notificationOpened, payload: userInfo) 272 | ) 273 | } 274 | completionHandler(.noData) 275 | case .active: 276 | sharedEventManager.sendEventToJS( 277 | PushEvent(type: NativeEvent.foregroundMessageReceived, payload: userInfo) 278 | ) 279 | completionHandler(.noData) 280 | @unknown default: break // we don't handle any possible new state added in the future for now 281 | } 282 | } 283 | } 284 | 285 | func completeNotification(_ completionHandlerId: String) { 286 | if let completionHandler = remoteNotificationCompletionHandlers[completionHandlerId] { 287 | completionHandler(.noData) 288 | remoteNotificationCompletionHandlers.removeValue(forKey: completionHandlerId) 289 | } 290 | } 291 | 292 | private func setUpObservers() { 293 | NotificationCenter.default.addObserver( 294 | self, 295 | selector: #selector(applicationDidBecomeActive), 296 | name: UIApplication.didBecomeActiveNotification, 297 | object: nil 298 | ) 299 | 300 | NotificationCenter.default.addObserver( 301 | self, 302 | selector: #selector(applicationDidEnterBackground), 303 | name: UIApplication.didEnterBackgroundNotification, 304 | object: nil 305 | ) 306 | } 307 | 308 | private func removeObservers() { 309 | NotificationCenter.default.removeObserver( 310 | self, 311 | name: UIApplication.didBecomeActiveNotification, 312 | object: nil 313 | ) 314 | 315 | NotificationCenter.default.removeObserver( 316 | self, 317 | name: UIApplication.didEnterBackgroundNotification, 318 | object: nil 319 | ) 320 | } 321 | 322 | @objc 323 | private func applicationDidBecomeActive() { 324 | registerForRemoteNotifications() 325 | } 326 | 327 | @objc 328 | private func applicationDidEnterBackground() { 329 | // When App enters background we remove the cached launchNotification 330 | // as when the App reopens after this point, there won't be a notification 331 | // that launched the App. 332 | launchNotification = nil 333 | } 334 | 335 | private func registerForRemoteNotifications() { 336 | if RCTRunningInAppExtension() { 337 | return 338 | } 339 | 340 | DispatchQueue.main.async { 341 | RCTSharedApplication()?.registerForRemoteNotifications() 342 | } 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | npm downloads 5 | 6 | 9 | discord users online 10 |
11 | 12 |
13 |
14 | bell 15 |
16 | 17 |

18 | Push for React Native 19 |

20 | 21 |
22 | 23 | ## Installation 24 | 25 | ```sh 26 | yarn add @candlefinance/push 27 | ``` 28 | 29 | The motivation to write this came from the unmaintained and outdated libraries that exist today. This implementation is written in Swift in less than 200 lines of code. 30 | 31 | Android support is coming soon. Check out [#1](https://github.com/candlefinance/push/issues/1) if you want to help. 32 | 33 | ## Usage 34 | 35 | ### iOS 36 | 37 | - [x] Request permissions 38 | - [x] Register for APNS token 39 | - [x] Remote push notifications 40 | - [x] Foreground 41 | - [x] Background 42 | - [x] Opened by tapping on the notification 43 | - [ ] Local push notifications 44 | 45 | #### Setup 46 | 47 | 1. You'll need to update your `AppDelegate.swift` to handle push check the example app [here](./example/ios/AppDelegate.swift) for an example. 48 | 2. If your AppDelegate is in Objective-C (`.mm|.m|.h`), create a new `AppDelegate.swift` file and bridging header, then delete the Objective-C AppDelegate and main.m file. Finally, copy the contents of the example app's [AppDelegate.swift](./example/ios/AppDelegate.swift) and [bridge header](./example/ios/PushExample-Bridging-Header.h) to your project. 49 | 3. Make sure you're on `iOS 15` or later. 50 | 4. You can also use Objective-C just add bridging header and import the module. 51 | 5. `UNUserNotificationCenterDelegate` set in AppDelegate. 52 | 53 | ### Android 54 | 55 | - [x] Request permissions 56 | - [x] Register for FCM token 57 | - [x] Remote push notifications 58 | - [x] Foreground 59 | - [x] Background + Headless JS 60 | - [x] Opened by tapping on the notification 61 | - [ ] Local push notifications 62 | 63 | #### Setup 64 | 65 | 1. Add permissions in [AndroidManifest.xml](./example/android/app/src/main/AndroidManifest.xml) 66 | 2. Add `google-services.json` in `android/app` directory from Firebase console. 67 | 68 | ## API 69 | 70 |
71 | The following code is used to handle push notifications on the React Native side: 72 | 73 | ```js 74 | import type { PushNotificationPermissionStatus } from '@candlefinance/push'; 75 | import { module as Push } from '@candlefinance/push'; 76 | 77 | // Shows dialog to request permission to send push notifications, gets APNS token 78 | const isGranted = await push.requestPermissions(); 79 | 80 | // Get the APNS token w/o showing permission, useful if you want silent push notifications 81 | push.registerForToken(); 82 | 83 | // Check permission status: 'granted', 'denied', or 'notDetermined' 84 | const status = await push.getAuthorizationStatus(); 85 | 86 | // Listeners 87 | React.useEffect(() => { 88 | const { NativeEvent, NativeHeadlessTaskKey } = Push.getConstants(); 89 | console.log(NativeEvent, NativeHeadlessTaskKey); 90 | Push.addTokenEventListener(NativeEvent.TOKEN_RECEIVED, (token) => { 91 | console.log('TOKEN_RECEIVED:', token); 92 | }); 93 | Push.addMessageEventListener( 94 | NativeEvent.BACKGROUND_MESSAGE_RECEIVED, 95 | (message, id) => { 96 | console.log('BACKGROUND_MESSAGE_RECEIVED:', message); 97 | if (id !== undefined) { 98 | console.log('Completing notification:', id); 99 | Push.completeNotification(id); 100 | } 101 | } 102 | ); 103 | Push.addErrorListener(NativeEvent.FAILED_TO_REGISTER, (message) => { 104 | console.log('FAILED_TO_REGISTER:', message); 105 | }); 106 | Push.addMessageEventListener(NativeEvent.NOTIFICATION_OPENED, (message) => { 107 | console.log('NOTIFICATION_OPENED:', message); 108 | }); 109 | Push.addMessageEventListener( 110 | NativeEvent.FOREGROUND_MESSAGE_RECEIVED, 111 | (message) => { 112 | console.log('FOREGROUND_MESSAGE_RECEIVED:', message); 113 | } 114 | ); 115 | Push.addMessageEventListener( 116 | NativeEvent.LAUNCH_NOTIFICATION_OPENED, 117 | (message) => { 118 | console.log('LAUNCH_NOTIFICATION_OPENED:', message); 119 | } 120 | ); 121 | return () => { 122 | Push.removeListeners(NativeEvent.TOKEN_RECEIVED); 123 | Push.removeListeners(NativeEvent.BACKGROUND_MESSAGE_RECEIVED); 124 | Push.removeListeners(NativeEvent.NOTIFICATION_OPENED); 125 | Push.removeListeners(NativeEvent.FOREGROUND_MESSAGE_RECEIVED); 126 | Push.removeListeners(NativeEvent.LAUNCH_NOTIFICATION_OPENED); 127 | }; 128 | }, []); 129 | ``` 130 | 131 | ## Testing 132 | 133 | If you run the example app, you can test push notifications by running the following command: 134 | 135 | ```sh 136 | yarn push 137 | ``` 138 | 139 | This will use the [payload.json](./example/payload.json) file to send a push notification to the device. You can modify the payload to test different scenarios. 140 | 141 | Apple also has a new [console](https://developer.apple.com/notifications/push-notifications-console/) to test push notifications. If you print out the token from `deviceTokenReceived` listener, you can use it to send a push notification from the console. 142 | 143 | ## SNS 144 | 145 | If you're using AWS SNS, you can use the following code to send a push notification 146 | 147 | ``` 148 | const message = // apns 149 | os === 'ios' ? JSON.stringify({ APNS: JSON.stringify(payload) }) 150 | : // fcm 151 | JSON.stringify({ 152 | GCM: JSON.stringify({ 153 | data: { 154 | title: title, 155 | body: body, 156 | custom: customData, 157 | data: customData, 158 | priority: '1', 159 | imageUrl: 160 | 'https://logo.png', 161 | targetClass: 'com.yourapp.candle.MainActivity', 162 | }, 163 | }) 164 | }) 165 | ``` 166 | 167 | ## Contributing 168 | 169 | We are open to contributions. Please read our [Contributing Guide](CONTRIBUTING.md) for more information. 170 | 171 | ## License 172 | 173 | This project is licensed under the terms of the [MIT license](LICENSE). 174 | 175 | ## Discord 176 | 177 | Post in #oss channel in our [Discord](https://discord.gg/Qm7ZPUhBWV) if you have any questions or want to contribute. 178 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifestNew.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 15 | 18 | 21 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/src/main/java/com/candlefinance/push/FirebaseMessagingService.kt: -------------------------------------------------------------------------------- 1 | package com.candlefinance.push 2 | 3 | import android.content.Intent 4 | import android.util.Log 5 | import com.facebook.react.HeadlessJsTaskService 6 | import com.facebook.react.bridge.Arguments 7 | import com.facebook.react.jstasks.HeadlessJsTaskConfig 8 | import com.google.firebase.messaging.FirebaseMessagingService 9 | import org.json.JSONObject 10 | 11 | private val TAG = PushModule::class.java.simpleName 12 | class PushNotificationHeadlessTaskService : HeadlessJsTaskService() { 13 | 14 | private val defaultTimeout: Long = 10000 // 10 seconds 15 | override fun getTaskConfig(intent: Intent): HeadlessJsTaskConfig? { 16 | return NotificationPayload.fromIntent(intent)?.let { 17 | Log.d(TAG, "Starting headless task with payload: ${it.rawData}") 18 | HeadlessJsTaskConfig( 19 | HEADLESS_TASK_KEY, 20 | it.toWritableMap(), 21 | defaultTimeout, true 22 | ) 23 | } 24 | } 25 | 26 | companion object { 27 | const val HEADLESS_TASK_KEY = "PushNotificationHeadlessTaskKey" 28 | } 29 | } 30 | 31 | class FirebaseMessagingService : FirebaseMessagingService() { 32 | 33 | private lateinit var utils: PushNotificationUtils 34 | 35 | override fun onCreate() { 36 | super.onCreate() 37 | utils = PushNotificationUtils(baseContext) 38 | } 39 | 40 | override fun onNewToken(token: String) { 41 | super.onNewToken(token) 42 | val params = Arguments.createMap() 43 | params.putString("token", token) 44 | Log.d(TAG, "Send device token event") 45 | PushNotificationEventManager.sendEvent(PushNotificationEventType.TOKEN_RECEIVED, params) 46 | } 47 | 48 | override fun handleIntent(intent: Intent) { 49 | val payload = NotificationPayload.fromIntent(intent) 50 | if (payload != null) { 51 | onMessageReceived(payload) 52 | Log.d(TAG, "Notification payload found in intent $payload") 53 | } else { 54 | Log.d(TAG, "No notification payload found in intent") 55 | } 56 | } 57 | 58 | private fun onMessageReceived(payload: NotificationPayload) { 59 | Log.d(TAG, "Received new message: $payload") 60 | if (utils.isAppInForeground()) { 61 | Log.d(TAG, "Send foreground message received event with payload: $payload") 62 | PushNotificationEventManager.sendEvent( 63 | PushNotificationEventType.FOREGROUND_MESSAGE_RECEIVED, payload.toWritableMap() 64 | ) 65 | } else if (utils.isAppInBackground()) { 66 | Log.d(TAG, "App is in background but in memory, send background message received event with payload: $payload") 67 | PushNotificationEventManager.sendEvent( 68 | PushNotificationEventType.BACKGROUND_MESSAGE_RECEIVED, payload.toWritableMap() 69 | ) 70 | utils.showNotification(payload) 71 | } else { 72 | Log.d(TAG, "App is killed, start HeadlessJsTaskService with payload: $payload") 73 | utils.showNotification(payload) 74 | 75 | try { 76 | val serviceIntent = Intent(baseContext, PushNotificationHeadlessTaskService::class.java) 77 | val json = JSONObject() 78 | json.put("rawData", payload.rawData) 79 | payload.rawData.forEach { (key, value) -> 80 | json.put(key, value) 81 | } 82 | serviceIntent.putExtra("NotificationPayload", json.toString()) 83 | if (baseContext.startService(serviceIntent) != null) { 84 | HeadlessJsTaskService.acquireWakeLockNow(baseContext) 85 | } else { 86 | Log.e(TAG, "Failed to start headless task") 87 | PushNotificationEventManager.sendEvent( 88 | PushNotificationEventType.BACKGROUND_MESSAGE_RECEIVED, payload.toWritableMap() 89 | ) 90 | } 91 | } catch (exception: Exception) { 92 | Log.e(TAG, "Something went wrong while starting headless task: ${exception.message}") 93 | PushNotificationEventManager.sendEvent( 94 | PushNotificationEventType.BACKGROUND_MESSAGE_RECEIVED, payload.toWritableMap() 95 | ) 96 | } 97 | } 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /android/src/main/java/com/candlefinance/push/NotificationUtils.kt: -------------------------------------------------------------------------------- 1 | package com.candlefinance.push 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.app.PendingIntent 7 | import android.content.Context 8 | import android.content.Intent 9 | import android.graphics.Bitmap 10 | import android.graphics.BitmapFactory 11 | import android.os.Build 12 | import android.os.Parcelable 13 | import android.util.Log 14 | import androidx.annotation.ChecksSdkIntAtLeast 15 | import androidx.core.app.NotificationCompat 16 | import androidx.core.content.ContextCompat 17 | import androidx.lifecycle.DefaultLifecycleObserver 18 | import androidx.lifecycle.LifecycleOwner 19 | import androidx.lifecycle.ProcessLifecycleOwner 20 | import com.facebook.react.bridge.Arguments 21 | import com.facebook.react.bridge.WritableMap 22 | import kotlinx.coroutines.CoroutineScope 23 | import kotlinx.coroutines.Dispatchers 24 | import kotlinx.coroutines.launch 25 | import kotlinx.coroutines.withContext 26 | import kotlinx.parcelize.IgnoredOnParcel 27 | import kotlinx.parcelize.Parcelize 28 | import org.json.JSONObject 29 | import java.net.URL 30 | 31 | class PushNotificationsConstants { 32 | companion object { 33 | const val OPENAPP = "openApp" // openApp 34 | const val URL = "url" // url 35 | const val DEEPLINK = "deeplink" // deeplink 36 | const val TITLE = "title" // title 37 | const val BODY = "body" // body 38 | const val SUBTITLE = "subtitle" // subtitle 39 | const val PRIORITY = "priority" // priority 40 | const val IMAGEURL = "imageUrl" // imageUrl 41 | const val DEFAULT_NOTIFICATION_CHANNEL_ID = "default_notification_channel_id" // default_notification_channel_id 42 | } 43 | } 44 | 45 | class PushNotificationsUtils( 46 | private val context: Context, 47 | private val channelId: String = PushNotificationsConstants.DEFAULT_NOTIFICATION_CHANNEL_ID 48 | ) { 49 | init { 50 | retrieveNotificationChannel() 51 | } 52 | 53 | @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) 54 | private fun isNotificationChannelSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O 55 | 56 | private fun retrieveNotificationChannel(): NotificationChannel? { 57 | var channel: NotificationChannel? = null 58 | val notificationManager = ContextCompat.getSystemService(context, NotificationManager::class.java) 59 | if (isNotificationChannelSupported()) { 60 | channel = notificationManager?.getNotificationChannel(channelId) 61 | } 62 | return channel ?: createDefaultNotificationChannel(channelId) 63 | } 64 | 65 | // create before notification trigger for API 32 or lower 66 | @SuppressLint("NewApi") 67 | private fun createDefaultNotificationChannel(channelId: String): NotificationChannel? { 68 | // Create the NotificationChannel, but only on API 26+ because 69 | // the NotificationChannel class is new and not in the support library 70 | if (isNotificationChannelSupported()) { 71 | val notificationManager = ContextCompat.getSystemService(context, NotificationManager::class.java) 72 | val defaultChannel = NotificationChannel( 73 | channelId, 74 | "Default channel", 75 | NotificationManager.IMPORTANCE_DEFAULT 76 | ) 77 | // Register the channel with the system 78 | notificationManager?.createNotificationChannel(defaultChannel) 79 | return defaultChannel 80 | } 81 | return null 82 | } 83 | 84 | private suspend fun downloadImage(url: String): Bitmap? = withContext(Dispatchers.IO) { 85 | BitmapFactory.decodeStream(URL(url).openConnection().getInputStream()) 86 | } 87 | 88 | @SuppressLint("DiscouragedApi") 89 | fun getResourceIdByName(name: String, type: String): Int { 90 | return context.resources.getIdentifier(name, type, context.packageName) 91 | } 92 | 93 | @SuppressLint("NewApi") 94 | fun showNotification( 95 | notificationId: Int, 96 | payload: NotificationPayload, 97 | targetClass: Class<*>? 98 | ) { 99 | Log.d(Tag, "Show notification with payload: ${payload.rawData}") 100 | CoroutineScope(Dispatchers.IO).launch { 101 | val notificationManager = ContextCompat.getSystemService(context, NotificationManager::class.java) 102 | val notificationBuilder = NotificationCompat.Builder(context, channelId) 103 | val notificationContent = payload.rawData 104 | 105 | notificationBuilder 106 | .setSmallIcon(getResourceIdByName("ic_default_notification_foreground", "drawable")) 107 | .setContentTitle(notificationContent[PushNotificationsConstants.TITLE]) 108 | .setContentText(notificationContent[PushNotificationsConstants.BODY]) 109 | .setSubText(notificationContent[PushNotificationsConstants.SUBTITLE]) 110 | .setPriority(notificationContent[PushNotificationsConstants.PRIORITY]?.toInt() ?: NotificationCompat.PRIORITY_DEFAULT) 111 | .setAutoCancel(true) 112 | 113 | Log.d(Tag, "targetClass: $targetClass") 114 | if (targetClass != null) { 115 | Log.d(Tag, "targetClass is not null") 116 | val intent = Intent(context, targetClass).apply { 117 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 118 | val url = notificationContent[PushNotificationsConstants.URL] 119 | val deepLink = notificationContent[PushNotificationsConstants.DEEPLINK] 120 | 121 | if (url != null) { 122 | putExtra(PushNotificationsConstants.URL, url) 123 | } 124 | 125 | if (deepLink != null) { 126 | putExtra(PushNotificationsConstants.DEEPLINK, deepLink) 127 | } 128 | putExtra(PushNotificationsConstants.OPENAPP, true) 129 | val json = JSONObject() 130 | notificationContent.forEach { (key, value) -> json.put(key, value) } 131 | Log.d(Tag, "SAVE to intent rawData: $json") 132 | putExtra("rawData", json.toString()) 133 | } 134 | 135 | notificationBuilder.setContentIntent( 136 | PendingIntent.getActivity( 137 | context, 138 | notificationId, 139 | intent, 140 | PendingIntent.FLAG_UPDATE_CURRENT or if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else 0 141 | ) 142 | ) 143 | } else { 144 | Log.e(Tag, "targetClass is null") 145 | } 146 | 147 | if (notificationContent.containsKey(PushNotificationsConstants.IMAGEURL)) { 148 | val imageUrl = notificationContent[PushNotificationsConstants.IMAGEURL] 149 | val bitmap = imageUrl?.let { downloadImage(it) } 150 | if (bitmap != null) { 151 | notificationBuilder.setLargeIcon(bitmap) 152 | } 153 | } 154 | 155 | if (isNotificationChannelSupported()) { 156 | notificationBuilder.setChannelId(channelId) 157 | } 158 | 159 | notificationManager?.notify(notificationId, notificationBuilder.build()) 160 | } 161 | } 162 | } 163 | 164 | private const val Tag = "PushNotificationUtils" 165 | 166 | class PushNotificationUtils(context: Context) { 167 | private val utils = PushNotificationsUtils(context) 168 | private val lifecycleObserver = AppLifecycleListener() 169 | 170 | init { 171 | if (context is LifecycleOwner) { 172 | Log.d(Tag, "Add lifecycle observer to context") 173 | context.lifecycle.addObserver(lifecycleObserver) 174 | } else { 175 | Log.e(Tag, "Context is not a lifecycle owner") 176 | ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleObserver) 177 | } 178 | } 179 | 180 | fun showNotification( 181 | payload: NotificationPayload 182 | ) { 183 | Log.d(Tag, "Show notification with payload: $payload") 184 | 185 | val notificationId = (System.currentTimeMillis() % Int.MAX_VALUE).toInt() 186 | val targetClass = payload.rawData["targetClass"]?.let { 187 | Log.d(Tag, "targetClass: $it") 188 | try { 189 | Class.forName(it) 190 | } catch (e: ClassNotFoundException) { 191 | Log.e(Tag, "Class not found: $it") 192 | null 193 | } 194 | } 195 | if (targetClass != null) { 196 | utils.showNotification(notificationId, payload, targetClass) 197 | } else { 198 | Log.e(Tag, "targetClass is null") 199 | } 200 | } 201 | 202 | fun isAppInForeground(): Boolean { 203 | return lifecycleObserver.isAppInForeground 204 | } 205 | 206 | fun isAppInBackground(): Boolean { 207 | return lifecycleObserver.isAppInBackground 208 | } 209 | } 210 | 211 | @Parcelize 212 | open class NotificationContentProvider internal constructor(open val content: Map) : Parcelable { 213 | @Parcelize 214 | class FCM(override val content: Map) : NotificationContentProvider(content) 215 | } 216 | 217 | @Parcelize 218 | open class NotificationPayload( 219 | private val contentProvider: NotificationContentProvider, 220 | val channelId: String? = null, 221 | val targetClass: Class<*>? = null 222 | ) : Parcelable { 223 | 224 | @IgnoredOnParcel 225 | val rawData: Map = extractRawData() 226 | 227 | internal constructor(builder: Builder) : this(builder.contentProvider, builder.channelId, builder.targetClass) 228 | 229 | private fun extractRawData() = when (contentProvider) { 230 | is NotificationContentProvider.FCM -> contentProvider.content 231 | else -> mapOf() 232 | } 233 | 234 | fun toWritableMap(): WritableMap { 235 | val map = Arguments.createMap() 236 | if (rawData.containsKey("rawData")) { 237 | val existingRawData = rawData["rawData"]?.let { 238 | JSONObject(it) 239 | } 240 | val toMap = existingRawData?.let { 241 | it.keys().asSequence().associateWith { key -> it.get(key).toString() } 242 | } 243 | toMap?.forEach { (key, value) -> map.putString(key, value) } 244 | map.putMap("rawData", Arguments.makeNativeMap(toMap)) 245 | Log.d(Tag, "TO READABLE existing: $map") 246 | } else { 247 | map.putMap("rawData", Arguments.makeNativeMap(rawData)) 248 | rawData.forEach { (key, value) -> map.putString(key, value) } 249 | Log.d(Tag, "TO READABLE new: $map") 250 | } 251 | map.putString("channelId", channelId) 252 | return map 253 | } 254 | 255 | companion object { 256 | @JvmStatic 257 | fun builder(contentProvider: NotificationContentProvider) = Builder(contentProvider) 258 | 259 | inline operator fun invoke( 260 | contentProvider: NotificationContentProvider, 261 | block: Builder.() -> Unit 262 | ) = Builder(contentProvider).apply(block).build() 263 | 264 | @JvmStatic 265 | fun fromIntent(intent: Intent?): NotificationPayload? { 266 | return intent?.extras?.let { 267 | val toMap: Map = it.keySet().associateWith { key -> it.get(key)?.toString() } 268 | val contentProvider = NotificationContentProvider.FCM(toMap.filterValues { it != null } as Map) 269 | Log.d(Tag, "READING: Notification payload from intent: $toMap") 270 | NotificationPayload(contentProvider) 271 | } 272 | } 273 | } 274 | 275 | class Builder(val contentProvider: NotificationContentProvider) { 276 | var channelId: String? = null 277 | private set 278 | var targetClass: Class<*>? = null 279 | private set 280 | 281 | fun notificationChannelId(channelId: String?) = apply { this.channelId = channelId } 282 | 283 | fun targetClass(targetClass: Class<*>?) = apply { this.targetClass = targetClass } 284 | 285 | fun build() = NotificationPayload(this) 286 | } 287 | } 288 | 289 | sealed interface PermissionRequestResult { 290 | data object Granted : PermissionRequestResult 291 | data class NotGranted(val shouldShowRationale: Boolean) : PermissionRequestResult 292 | } 293 | 294 | class AppLifecycleListener : DefaultLifecycleObserver { 295 | var isAppInForeground: Boolean = false 296 | var isAppInBackground: Boolean = false 297 | 298 | override fun onStart(owner: LifecycleOwner) { 299 | // App moved to foreground 300 | println("App is in the foreground") 301 | 302 | isAppInForeground = true 303 | isAppInBackground = false 304 | } 305 | 306 | override fun onStop(owner: LifecycleOwner) { 307 | // App moved to background 308 | println("App is in the background") 309 | isAppInBackground = true 310 | isAppInForeground = false 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /android/src/main/java/com/candlefinance/push/PushModule.kt: -------------------------------------------------------------------------------- 1 | package com.candlefinance.push 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.content.Context 6 | import android.content.Context.MODE_PRIVATE 7 | import android.content.Intent 8 | import android.content.pm.PackageManager 9 | import android.os.Build 10 | import android.os.Handler 11 | import android.os.Looper 12 | import android.util.Log 13 | import androidx.core.app.ActivityCompat 14 | import androidx.core.content.ContextCompat 15 | import com.facebook.react.bridge.ActivityEventListener 16 | import com.facebook.react.bridge.Arguments 17 | import com.facebook.react.bridge.LifecycleEventListener 18 | import com.facebook.react.bridge.Promise 19 | import com.facebook.react.bridge.ReactApplicationContext 20 | import com.facebook.react.bridge.ReactContextBaseJavaModule 21 | import com.facebook.react.bridge.ReactMethod 22 | import com.facebook.react.bridge.ReadableMap 23 | import com.facebook.react.bridge.WritableMap 24 | import com.google.android.gms.tasks.OnCompleteListener 25 | import com.google.firebase.messaging.FirebaseMessaging 26 | import kotlinx.coroutines.CoroutineDispatcher 27 | import kotlinx.coroutines.CoroutineScope 28 | import kotlinx.coroutines.Dispatchers 29 | import kotlinx.coroutines.cancel 30 | import kotlinx.coroutines.channels.BufferOverflow 31 | import kotlinx.coroutines.flow.MutableSharedFlow 32 | import kotlinx.coroutines.flow.filter 33 | import kotlinx.coroutines.flow.first 34 | import kotlinx.coroutines.flow.map 35 | import kotlinx.coroutines.launch 36 | import java.util.UUID 37 | 38 | private val TAG = PushModule::class.java.simpleName 39 | private const val PERMISSION = "android.permission.POST_NOTIFICATIONS" 40 | private const val PREF_FILE_KEY = "com.candlefinance.push.pushnotification" 41 | private const val PREF_PREVIOUSLY_DENIED = "wasPermissionPreviouslyDenied" 42 | 43 | enum class PushNotificationPermissionStatus { 44 | NotDetermined, 45 | Authorized, 46 | Denied, 47 | } 48 | 49 | class PushModule( 50 | reactContext: ReactApplicationContext, 51 | dispatcher: CoroutineDispatcher = Dispatchers.Main 52 | ) : ReactContextBaseJavaModule(reactContext), ActivityEventListener, LifecycleEventListener { 53 | 54 | override fun getName(): String { 55 | return NAME 56 | } 57 | 58 | companion object { 59 | const val NAME = "Push" 60 | } 61 | 62 | private var launchNotification: WritableMap? = null 63 | private val sharedPreferences = reactContext.getSharedPreferences(PREF_FILE_KEY, MODE_PRIVATE) 64 | private val scope = CoroutineScope(dispatcher) 65 | 66 | init { 67 | reactContext.addActivityEventListener(this) 68 | reactContext.addLifecycleEventListener(this) 69 | } 70 | 71 | @ReactMethod 72 | fun registerForToken(promise: Promise) { 73 | FirebaseMessaging.getInstance().isAutoInitEnabled=true; 74 | FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task -> 75 | if (!task.isSuccessful) { 76 | Log.d(TAG,"Fetching FCM registration token failed ${task.exception?.message}") 77 | return@OnCompleteListener 78 | } 79 | val token = task.result 80 | PushNotificationEventManager.sendEvent(PushNotificationEventType.TOKEN_RECEIVED, Arguments.createMap().apply { 81 | putString("token", token) 82 | }) 83 | }) 84 | promise.resolve(true) 85 | } 86 | 87 | @ReactMethod 88 | fun getLaunchNotification(promise: Promise) { 89 | launchNotification?.let { 90 | promise.resolve(launchNotification) 91 | launchNotification = null 92 | } ?: promise.resolve(null) 93 | } 94 | 95 | @ReactMethod 96 | fun getPermissionStatus(promise: Promise) { 97 | val permission = reactApplicationContext.currentActivity?.let { PushNotificationPermission(it) } 98 | // If permission has already been granted 99 | if (permission != null) { 100 | if (permission.hasRequiredPermission) { 101 | return promise.resolve(PushNotificationPermissionStatus.Authorized.name) 102 | } 103 | } 104 | // If the shouldShowRequestPermissionRationale flag is true, permission must have been 105 | // denied once (and only once) previously 106 | if (shouldShowRequestPermissionRationale()) { 107 | return promise.resolve(PushNotificationPermissionStatus.NotDetermined.name) 108 | } 109 | // If the shouldShowRequestPermissionRationale flag is false and the permission was 110 | // already previously denied then user has denied permissions twice 111 | if (sharedPreferences.getBoolean(PREF_PREVIOUSLY_DENIED, false)) { 112 | return promise.resolve(PushNotificationPermissionStatus.Denied.name) 113 | } 114 | // Otherwise it's never been requested (or user could have dismissed the request without 115 | // explicitly denying) 116 | promise.resolve(PushNotificationPermissionStatus.NotDetermined.name) 117 | } 118 | 119 | @ReactMethod 120 | fun requestPermissions( 121 | @Suppress("UNUSED_PARAMETER") permissions: ReadableMap, 122 | promise: Promise 123 | ) { 124 | scope.launch { 125 | val permission = reactApplicationContext.currentActivity?.let { PushNotificationPermission(it) } 126 | val result = permission?.requestPermission() 127 | if (result is PermissionRequestResult.Granted) { 128 | promise.resolve(true) 129 | } else { 130 | // If permission was not granted and the shouldShowRequestPermissionRationale flag 131 | // is true then user must have denied for the first time. We will set the 132 | // wasPermissionPreviouslyDenied value to true only in this scenario since it's 133 | // possible to dismiss the permission request without explicitly denying as well. 134 | if (shouldShowRequestPermissionRationale()) { 135 | with(sharedPreferences.edit()) { 136 | putBoolean(PREF_PREVIOUSLY_DENIED, true) 137 | apply() 138 | } 139 | } 140 | promise.resolve(false) 141 | } 142 | } 143 | } 144 | 145 | @ReactMethod 146 | fun addListener(@Suppress("UNUSED_PARAMETER") eventName: String) { 147 | // noop - only required for RN NativeEventEmitter 148 | } 149 | 150 | @ReactMethod 151 | fun removeListeners(@Suppress("UNUSED_PARAMETER") count: Int) { 152 | // noop - only required for RN NativeEventEmitter 153 | } 154 | 155 | override fun getConstants(): MutableMap = hashMapOf( 156 | "NativeEvent" to PushNotificationEventType.values().associateBy({ it.name }, { it.value }), 157 | "NativeHeadlessTaskKey" to PushNotificationHeadlessTaskService.HEADLESS_TASK_KEY 158 | ) 159 | 160 | override fun onActivityResult(p0: Activity?, p1: Int, p2: Int, p3: Intent?) { 161 | // noop - only overridden as this class implements ActivityEventListener 162 | } 163 | 164 | /** 165 | * Send notification opened app event to JS layer if the app is in a background state 166 | */ 167 | override fun onNewIntent(intent: Intent) { 168 | Log.d(TAG, "New intent received") 169 | val payload = NotificationPayload.fromIntent(intent) 170 | if (payload != null) { 171 | PushNotificationEventManager.sendEvent( 172 | PushNotificationEventType.NOTIFICATION_OPENED, payload.toWritableMap() 173 | ) 174 | } else { 175 | Log.d(TAG, "No notification payload found in intent") 176 | } 177 | } 178 | 179 | /** 180 | * On every app resume (including launch), send the current device token to JS layer. Also 181 | * store the app launching notification if app is in a quit state 182 | */ 183 | override fun onHostResume() { 184 | Log.d(TAG, "App resumed") 185 | PushNotificationEventManager.init(reactApplicationContext) 186 | currentActivity?.intent?.let { 187 | val payload = NotificationPayload.fromIntent(it) 188 | if (payload != null) { 189 | Log.d(TAG, "Launch notification found in intent waiting 5 seconds") 190 | launchNotification = payload.toWritableMap() 191 | Handler(Looper.getMainLooper()).postDelayed({ 192 | PushNotificationEventManager.sendEvent( 193 | PushNotificationEventType.LAUNCH_NOTIFICATION_OPENED, 194 | payload.toWritableMap() 195 | ) 196 | }, 3000) 197 | } else { 198 | Log.d(TAG, "No launch notification found in intent") 199 | } 200 | } 201 | } 202 | 203 | override fun onHostPause() { 204 | // noop - only overridden as this class implements LifecycleEventListener 205 | Log.d(TAG, "App paused") 206 | } 207 | 208 | override fun onHostDestroy() { 209 | scope.cancel() 210 | } 211 | 212 | private fun shouldShowRequestPermissionRationale(): Boolean { 213 | return ActivityCompat.shouldShowRequestPermissionRationale(currentActivity!!, PERMISSION) 214 | } 215 | } 216 | 217 | internal const val PermissionRequiredApiLevel = 33 218 | internal const val PermissionName = "android.permission.POST_NOTIFICATIONS" 219 | 220 | class PushNotificationPermission(private val context: Context) { 221 | 222 | val hasRequiredPermission: Boolean 223 | get() = Build.VERSION.SDK_INT < PermissionRequiredApiLevel || 224 | ContextCompat.checkSelfPermission(context, PermissionName) == PackageManager.PERMISSION_GRANTED 225 | 226 | /** 227 | * Launches an Activity to request notification permissions and suspends until the user makes a selection or 228 | * dismisses the dialog. The behavior of this function depends on the device, current permission status, and 229 | * build configuration. 230 | * 231 | * 1. If the device API level is < 33 then this will immediately return [PermissionRequestResult.Granted] because 232 | * no permission is required on this device. 233 | * 2. If the device API level is >= 33 but the application is targeting API level < 33 then this function will not 234 | * show a permission dialog, but will return the current status of the notification permission. The permission 235 | * request dialog will instead appear whenever the app tries to create a notification channel. 236 | * 3. Otherwise, the dialog will be shown or not as per normal runtime permission request rules 237 | * See https://developer.android.com/develop/ui/views/notifications/notification-permission for details 238 | */ 239 | suspend fun requestPermission(): PermissionRequestResult { 240 | if (hasRequiredPermission) { 241 | return PermissionRequestResult.Granted 242 | } 243 | 244 | val requestId = UUID.randomUUID().toString() 245 | Log.d(TAG, "Requesting notification permission with requestId: $requestId") 246 | // Check if the context is an instance of Activity 247 | if (context is Activity) { 248 | Log.d(TAG, "Requesting notification permission") 249 | // Check if the version is Android 12 or higher 250 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 251 | Log.d(TAG, "Requesting notification permission on Android 12 or higher") 252 | // Request the permission 253 | Log.d(TAG, "Requesting notification permission on Android 12 or higher") 254 | ActivityCompat.requestPermissions( 255 | context, 256 | arrayOf(Manifest.permission.POST_NOTIFICATIONS), 257 | Math.abs(requestId.hashCode()) 258 | ) 259 | Log.d(TAG, "Requesting notification permission on Android 12 or higher") 260 | } 261 | 262 | // Listen for the result 263 | return PermissionRequestChannel.listen(requestId).first() 264 | } else { 265 | Log.e(TAG, "Context is not an instance of Activity") 266 | throw IllegalStateException("Context is not an instance of Activity") 267 | } 268 | } 269 | } 270 | 271 | internal object PermissionRequestChannel { 272 | private class IdAndResult(val requestId: String, val result: PermissionRequestResult) 273 | 274 | private val flow = MutableSharedFlow( 275 | extraBufferCapacity = 1, 276 | onBufferOverflow = BufferOverflow.DROP_OLDEST 277 | ) 278 | 279 | /** 280 | * Get a flow for the result of a particular permission request 281 | */ 282 | fun listen(requestId: String) = flow.filter { it.requestId == requestId }.map { it.result } 283 | 284 | /** 285 | * Send the result of a permission request 286 | */ 287 | fun send(requestId: String, result: PermissionRequestResult) = flow.tryEmit(IdAndResult(requestId, result)) 288 | } 289 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/src/main/res/drawable-anydpi-v24/ic_default_notification.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /android/src/main/res/drawable-hdpi/ic_default_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/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/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/android/src/main/res/drawable-mdpi/ic_default_notification.png -------------------------------------------------------------------------------- /android/src/main/res/drawable-xhdpi/ic_default_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/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/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/android/src/main/res/drawable-xxhdpi/ic_default_notification.png -------------------------------------------------------------------------------- /android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | fcm_default_channel 3 | FCM Default Channel 4 | 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:@react-native/babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | apply plugin: "org.jetbrains.kotlin.android" 3 | apply plugin: "com.facebook.react" 4 | apply plugin: "com.google.gms.google-services" 5 | 6 | /** 7 | * This is the configuration block to customize your React Native Android app. 8 | * By default you don't need to apply any configuration, just uncomment the lines you need. 9 | */ 10 | react { 11 | /* Folders */ 12 | // The root of your project, i.e. where "package.json" lives. Default is '..' 13 | // root = file("../") 14 | // The folder where the react-native NPM package is. Default is ../node_modules/react-native 15 | // reactNativeDir = file("../node_modules/react-native") 16 | // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen 17 | // codegenDir = file("../node_modules/@react-native/codegen") 18 | // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js 19 | // cliFile = file("../node_modules/react-native/cli.js") 20 | 21 | /* Variants */ 22 | // The list of variants to that are debuggable. For those we're going to 23 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'. 24 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. 25 | // debuggableVariants = ["liteDebug", "prodDebug"] 26 | 27 | /* Bundling */ 28 | // A list containing the node command and its flags. Default is just 'node'. 29 | // nodeExecutableAndArgs = ["node"] 30 | // 31 | // The command to run when bundling. By default is 'bundle' 32 | // bundleCommand = "ram-bundle" 33 | // 34 | // The path to the CLI configuration file. Default is empty. 35 | // bundleConfig = file(../rn-cli.config.js) 36 | // 37 | // The name of the generated asset file containing your JS bundle 38 | // bundleAssetName = "MyApplication.android.bundle" 39 | // 40 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' 41 | // entryFile = file("../js/MyApplication.android.js") 42 | // 43 | // A list of extra flags to pass to the 'bundle' commands. 44 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle 45 | // extraPackagerArgs = [] 46 | 47 | /* Hermes Commands */ 48 | // The hermes compiler command to run. By default it is 'hermesc' 49 | // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" 50 | // 51 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" 52 | // hermesFlags = ["-O", "-output-source-map"] 53 | } 54 | 55 | /** 56 | * Set this to true to Run Proguard on Release builds to minify the Java bytecode. 57 | */ 58 | def enableProguardInReleaseBuilds = false 59 | 60 | /** 61 | * The preferred build flavor of JavaScriptCore (JSC) 62 | * 63 | * For example, to use the international variant, you can use: 64 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` 65 | * 66 | * The international variant includes ICU i18n library and necessary data 67 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that 68 | * give correct results when using with locales other than en-US. Note that 69 | * this variant is about 6MiB larger per architecture than default. 70 | */ 71 | def jscFlavor = 'org.webkit:android-jsc:+' 72 | 73 | android { 74 | ndkVersion rootProject.ext.ndkVersion 75 | buildToolsVersion rootProject.ext.buildToolsVersion 76 | compileSdk rootProject.ext.compileSdkVersion 77 | 78 | namespace "com.pushexample" 79 | defaultConfig { 80 | applicationId "com.pushexample" 81 | minSdkVersion rootProject.ext.minSdkVersion 82 | targetSdkVersion rootProject.ext.targetSdkVersion 83 | versionCode 1 84 | versionName "1.0" 85 | } 86 | signingConfigs { 87 | debug { 88 | storeFile file('debug.keystore') 89 | storePassword 'android' 90 | keyAlias 'androiddebugkey' 91 | keyPassword 'android' 92 | } 93 | } 94 | buildTypes { 95 | debug { 96 | signingConfig signingConfigs.debug 97 | } 98 | release { 99 | // Caution! In production, you need to generate your own keystore file. 100 | // see https://reactnative.dev/docs/signed-apk-android. 101 | signingConfig signingConfigs.debug 102 | minifyEnabled enableProguardInReleaseBuilds 103 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 104 | } 105 | } 106 | } 107 | 108 | dependencies { 109 | // The version of react-native is set by the React Native Gradle Plugin 110 | implementation("com.facebook.react:react-android") 111 | implementation("com.facebook.react:flipper-integration") 112 | 113 | if (hermesEnabled.toBoolean()) { 114 | implementation("com.facebook.react:hermes-android") 115 | } else { 116 | implementation jscFlavor 117 | } 118 | } 119 | 120 | apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) 121 | -------------------------------------------------------------------------------- /example/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/example/android/app/debug.keystore -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 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 | -------------------------------------------------------------------------------- /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/app/src/main/res/drawable-anydpi-v24/ic_stat_name.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /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/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | PushExample 3 | 4 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/candlefinance/push/8ec7a3bfaf00089a6cee0e49b030dc9760ae2f9e/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /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/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command; 206 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 207 | # shell script including quotes and variable substitutions, so put them in 208 | # double quotes to make sure that they get re-expanded; and 209 | # * put everything else in single quotes, so that it's not re-expanded. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PushExample", 3 | "displayName": "PushExample" 4 | } 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/ios/.xcode.env.local: -------------------------------------------------------------------------------- 1 | export NODE_BINARY=/Users/gary/.nvm/versions/node/v18.16.0/bin/node 2 | 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/ios/File.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // PushExample 4 | // 5 | 6 | import Foundation 7 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/ios/PushExample/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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/ios/PushExample/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/ios/PushExample/PushExample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /example/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | }; 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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 |