diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index f918c91..ef95e58 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -5,13 +5,13 @@ runs: using: composite steps: - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version-file: .nvmrc - name: Cache dependencies id: yarn-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | **/node_modules diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39efde8..2625710 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,11 +12,21 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup + - name: Generate Env.tsx + env: + COURIER_BRAND_ID: ${{ secrets.COURIER_BRAND_ID }} + COURIER_AUTH_KEY: ${{ secrets.COURIER_AUTH_KEY }} + COURIER_CLIENT_KEY: ${{ secrets.COURIER_CLIENT_KEY }} + COURIER_TOPIC_ID: ${{ secrets.COURIER_TOPIC_ID }} + run: | + printf "export default class Env {\n static readonly brandId = '%s';\n static readonly authKey = '%s';\n static readonly clientKey = '%s';\n static readonly topicId = '%s';\n}\n" "$COURIER_BRAND_ID" "$COURIER_AUTH_KEY" "$COURIER_CLIENT_KEY" "$COURIER_TOPIC_ID" > example/src/Env.tsx + npx prettier --write example/src/Env.tsx + - name: Lint files run: yarn lint @@ -27,11 +37,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup + - name: Generate Env.tsx + env: + COURIER_BRAND_ID: ${{ secrets.COURIER_BRAND_ID }} + COURIER_AUTH_KEY: ${{ secrets.COURIER_AUTH_KEY }} + COURIER_CLIENT_KEY: ${{ secrets.COURIER_CLIENT_KEY }} + COURIER_TOPIC_ID: ${{ secrets.COURIER_TOPIC_ID }} + run: | + printf "export default class Env {\n static readonly brandId = '%s';\n static readonly authKey = '%s';\n static readonly clientKey = '%s';\n static readonly topicId = '%s';\n}\n" "$COURIER_BRAND_ID" "$COURIER_AUTH_KEY" "$COURIER_CLIENT_KEY" "$COURIER_TOPIC_ID" > example/src/Env.tsx + - name: Run unit tests run: yarn test --maxWorkers=2 --coverage @@ -39,7 +58,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup @@ -47,105 +66,3 @@ jobs: - name: Build package run: yarn prepack - build-android: - runs-on: ubuntu-latest - env: - TURBO_CACHE_DIR: .turbo/android - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup - uses: ./.github/actions/setup - - - name: Cache turborepo for Android - uses: actions/cache@v3 - with: - path: ${{ env.TURBO_CACHE_DIR }} - key: ${{ runner.os }}-turborepo-android-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-turborepo-android- - - - name: Check turborepo cache for Android - run: | - TURBO_CACHE_STATUS=$(node -p "($(yarn --silent turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android').cache.status") - - if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then - echo "turbo_cache_hit=1" >> $GITHUB_ENV - fi - - - name: Install JDK - if: env.turbo_cache_hit != 1 - uses: actions/setup-java@v3 - with: - distribution: 'zulu' - java-version: '11' - - - name: Finalize Android SDK - if: env.turbo_cache_hit != 1 - run: | - /bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null" - - - name: Cache Gradle - if: env.turbo_cache_hit != 1 - uses: actions/cache@v3 - with: - path: | - ~/.gradle/wrapper - ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - - - name: Build example for Android - run: | - yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" - - build-ios: - runs-on: macos-latest - env: - TURBO_CACHE_DIR: .turbo/ios - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup - uses: ./.github/actions/setup - - - name: Cache turborepo for iOS - uses: actions/cache@v3 - with: - path: ${{ env.TURBO_CACHE_DIR }} - key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-turborepo-ios- - - - name: Check turborepo cache for iOS - run: | - TURBO_CACHE_STATUS=$(node -p "($(yarn --silent turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status") - - if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then - echo "turbo_cache_hit=1" >> $GITHUB_ENV - fi - - - name: Cache cocoapods - if: env.turbo_cache_hit != 1 - id: cocoapods-cache - uses: actions/cache@v3 - with: - path: | - **/ios/Pods - key: ${{ runner.os }}-cocoapods-${{ hashFiles('example/ios/Podfile.lock') }} - restore-keys: | - ${{ runner.os }}-cocoapods- - - - name: Install cocoapods - if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' - run: | - yarn example pods - env: - NO_FLIPPER: 1 - - - name: Build example for iOS - run: | - yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..76a8e26 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,99 @@ +name: Deploy + +on: + push: + branches: [main] + +jobs: + deploy: + name: Deploy to npm + runs-on: ubuntu-latest + permissions: + contents: write + outputs: + deployed: ${{ steps.check.outputs.deployed }} + version: ${{ steps.check.outputs.version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + registry-url: 'https://registry.npmjs.org' + + - name: Check version + id: check + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Version: $VERSION" + + PUBLISHED=$(npm view @trycourier/courier-react-native versions --json 2>/dev/null || echo "[]") + if echo "$PUBLISHED" | python3 -c "import sys,json; vs=json.load(sys.stdin); sys.exit(0 if '$VERSION' in vs else 1)" 2>/dev/null; then + echo "Version $VERSION already published on npm. Skipping deploy." + echo "deployed=false" >> "$GITHUB_OUTPUT" + else + echo "Version $VERSION not yet published. Deploying." + echo "deployed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Install dependencies + if: steps.check.outputs.deployed == 'true' + run: yarn install --frozen-lockfile + + - name: Build package + if: steps.check.outputs.deployed == 'true' + run: yarn prepack + + - name: Create git tag + if: steps.check.outputs.deployed == 'true' + run: | + VERSION="${{ steps.check.outputs.version }}" + git tag "$VERSION" + git push origin "$VERSION" + + - name: Generate release notes + if: steps.check.outputs.deployed == 'true' + id: notes + run: | + PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || git rev-list --max-parents=0 HEAD) + NOTES=$(git log "$PREVIOUS_TAG"..HEAD --pretty=format:"- %s" --no-merges) + echo "notes<> "$GITHUB_OUTPUT" + echo "$NOTES" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + - name: Create GitHub release + if: steps.check.outputs.deployed == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + VERSION="${{ steps.check.outputs.version }}" + gh release create "$VERSION" --notes "${{ steps.notes.outputs.notes }}" + + - name: Publish to npm + if: steps.check.outputs.deployed == 'true' + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + test: + name: Run Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + + - name: Setup + uses: ./.github/actions/setup + + - name: Run unit tests + run: yarn test --maxWorkers=2 --coverage diff --git a/.github/workflows/test-status-report.yml b/.github/workflows/test-status-report.yml deleted file mode 100644 index 92c9ca5..0000000 --- a/.github/workflows/test-status-report.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Test Status Report - -on: - repository_dispatch: - types: [test-status] - -jobs: - update-status: - runs-on: ubuntu-latest - steps: - - name: Echo test status - run: echo "Tests for ${{ github.event.client_payload.source }} ${{ github.event.client_payload.status }}" - - - name: Fail if tests failed - run: | - if [[ "${{ github.event.client_payload.status }}" == "failed" ]]; then - echo "Tests failed. Exiting with error." - exit 1 - fi - echo "Tests passed." \ No newline at end of file diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml deleted file mode 100644 index 983593c..0000000 --- a/.github/workflows/workflow.yml +++ /dev/null @@ -1,106 +0,0 @@ -name: Test Trigger - -on: - push: - branches: [master] - workflow_dispatch: - pull_request: - branches: [master] - -jobs: - tests-on-push: - if: github.event_name == 'push' - runs-on: ubuntu-latest - steps: - - name: Dispatch App Build - run: | - curl -sS --fail-with-body -X POST \ - -H "Authorization: token ${{ secrets.AUTOMATION_ACCESS_TOKEN }}" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/repos/trycourier/mobile-automation-tests/dispatches \ - -d '{"event_type": "iOS-ReactNativeClassic", "client_payload": {"sha":"'"${{ github.sha }}"'","ref":"'"${{ github.ref }}"'"}}' - curl -sS --fail-with-body -X POST \ - -H "Authorization: token ${{ secrets.AUTOMATION_ACCESS_TOKEN }}" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/repos/trycourier/mobile-automation-tests/dispatches \ - -d '{"event_type": "Android-ReactNativeClassic", "client_payload": {"sha":"'"${{ github.sha }}"'","ref":"'"${{ github.ref }}"'"}}' - curl -sS --fail-with-body -X POST \ - -H "Authorization: token ${{ secrets.AUTOMATION_ACCESS_TOKEN }}" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/repos/trycourier/mobile-automation-tests/dispatches \ - -d '{"event_type": "iOS-ReactNativeExpo", "client_payload": {"sha":"'"${{ github.sha }}"'","ref":"'"${{ github.ref }}"'"}}' - curl -sS --fail-with-body -X POST \ - -H "Authorization: token ${{ secrets.AUTOMATION_ACCESS_TOKEN }}" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/repos/trycourier/mobile-automation-tests/dispatches \ - -d '{"event_type": "Android-ReactNativeExpo", "client_payload": {"sha":"'"${{ github.sha }}"'","ref":"'"${{ github.ref }}"'"}}' - - tests-on-pr: - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - name: Dispatch App Build - run: | - curl -sS --fail-with-body -X POST \ - -H "Authorization: token ${{ secrets.AUTOMATION_ACCESS_TOKEN }}" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/repos/trycourier/mobile-automation-tests/dispatches \ - -d "$(jq -n \ - --arg et 'iOS-ReactNativeClassic' \ - --arg prnum '${{ github.event.pull_request.number }}' \ - --arg sh '${{ github.event.pull_request.head.sha }}' \ - '{event_type:$et, client_payload:{pr:$prnum, sha:$sh}}')" - curl -sS --fail-with-body -X POST \ - -H "Authorization: token ${{ secrets.AUTOMATION_ACCESS_TOKEN }}" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/repos/trycourier/mobile-automation-tests/dispatches \ - -d "$(jq -n \ - --arg et 'Android-ReactNativeClassic' \ - --arg prnum '${{ github.event.pull_request.number }}' \ - --arg sh '${{ github.event.pull_request.head.sha }}' \ - '{event_type:$et, client_payload:{pr:$prnum, sha:$sh}}')" - curl -sS --fail-with-body -X POST \ - -H "Authorization: token ${{ secrets.AUTOMATION_ACCESS_TOKEN }}" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/repos/trycourier/mobile-automation-tests/dispatches \ - -d "$(jq -n \ - --arg et 'iOS-ReactNativeExpo' \ - --arg prnum '${{ github.event.pull_request.number }}' \ - --arg sh '${{ github.event.pull_request.head.sha }}' \ - '{event_type:$et, client_payload:{pr:$prnum, sha:$sh}}')" - curl -sS --fail-with-body -X POST \ - -H "Authorization: token ${{ secrets.AUTOMATION_ACCESS_TOKEN }}" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/repos/trycourier/mobile-automation-tests/dispatches \ - -d "$(jq -n \ - --arg et 'Android-ReactNativeExpo' \ - --arg prnum '${{ github.event.pull_request.number }}' \ - --arg sh '${{ github.event.pull_request.head.sha }}' \ - '{event_type:$et, client_payload:{pr:$prnum, sha:$sh}}')" - - tests-manual: - if: github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - steps: - - name: Dispatch App Build - run: | - curl -X POST \ - -H "Authorization: token ${{ secrets.AUTOMATION_ACCESS_TOKEN }}" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/repos/trycourier/mobile-automation-tests/dispatches \ - -d '{"event_type": "iOS-ReactNativeClassic", "client_payload": {"sha":"'"${{ github.sha }}"'","ref":"'"${{ github.ref }}"'"}}' - curl -X POST \ - -H "Authorization: token ${{ secrets.AUTOMATION_ACCESS_TOKEN }}" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/repos/trycourier/mobile-automation-tests/dispatches \ - -d '{"event_type": "Android-ReactNativeClassic", "client_payload": {"sha":"'"${{ github.sha }}"'","ref":"'"${{ github.ref }}"'"}}' - curl -X POST \ - -H "Authorization: token ${{ secrets.AUTOMATION_ACCESS_TOKEN }}" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/repos/trycourier/mobile-automation-tests/dispatches \ - -d '{"event_type": "iOS-ReactNativeExpo", "client_payload": {"sha":"'"${{ github.sha }}"'","ref":"'"${{ github.ref }}"'"}}' - curl -X POST \ - -H "Authorization: token ${{ secrets.AUTOMATION_ACCESS_TOKEN }}" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/repos/trycourier/mobile-automation-tests/dispatches \ - -d '{"event_type": "Android-ReactNativeExpo", "client_payload": {"sha":"'"${{ github.sha }}"'","ref":"'"${{ github.ref }}"'"}}' diff --git a/.gitignore b/.gitignore index a9874c8..9806368 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ example/android/app/release/ .expo/ # VSCode -.vscode/ jsconfig.json # Xcode @@ -48,6 +47,9 @@ android.iml # example/ios/Pods +# Local Xcode env (machine-specific node path) +example/ios/.xcode.env.local + # Ruby example/vendor/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f27bc57 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,47 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Bump Version", + "type": "node-terminal", + "request": "launch", + "command": "sh scripts/bump_version.sh; exit", + "cwd": "${workspaceFolder}" + }, + { + "name": "Build Demo App", + "type": "node-terminal", + "request": "launch", + "command": "sh scripts/build_demo_app.sh; exit", + "cwd": "${workspaceFolder}" + }, + { + "name": "Run iOS (Simulator)", + "type": "node-terminal", + "request": "launch", + "command": "cd example && npx react-native run-ios; exit", + "cwd": "${workspaceFolder}" + }, + { + "name": "Run iOS (Device)", + "type": "node-terminal", + "request": "launch", + "command": "cd example && npx react-native run-ios --device; exit", + "cwd": "${workspaceFolder}" + }, + { + "name": "Run Android (Emulator)", + "type": "node-terminal", + "request": "launch", + "command": "cd example && npx react-native run-android; exit", + "cwd": "${workspaceFolder}" + }, + { + "name": "Run Android (Device)", + "type": "node-terminal", + "request": "launch", + "command": "cd example && npx react-native run-android --active-arch-only; exit", + "cwd": "${workspaceFolder}" + } + ] +} diff --git a/.yarnrc b/.yarnrc deleted file mode 100644 index fedc0f1..0000000 --- a/.yarnrc +++ /dev/null @@ -1,3 +0,0 @@ -# Override Yarn command so we can automatically setup the repo on running `yarn` - -yarn-path "scripts/bootstrap.js" diff --git a/android/src/main/java/com/courierreactnative/Utils.kt b/android/src/main/java/com/courierreactnative/Utils.kt index e26ca08..9885df3 100644 --- a/android/src/main/java/com/courierreactnative/Utils.kt +++ b/android/src/main/java/com/courierreactnative/Utils.kt @@ -15,7 +15,7 @@ import com.facebook.react.modules.core.DeviceEventManagerModule import com.google.gson.GsonBuilder internal object Utils { - val COURIER_AGENT = CourierAgent.ReactNativeAndroid(version = "5.6.17") + val COURIER_AGENT = CourierAgent.ReactNativeAndroid(version = "5.7.0") } internal fun ReactContext.sendEvent(eventName: String, value: Any?) { diff --git a/example/babel.config.js b/example/babel.config.js index f6d9db7..f842b77 100644 --- a/example/babel.config.js +++ b/example/babel.config.js @@ -1,3 +1,3 @@ module.exports = { presets: ['module:metro-react-native-babel-preset'], -}; \ No newline at end of file +}; diff --git a/example/ios/.xcode.env.local b/example/ios/.xcode.env.local deleted file mode 100644 index b2fb97b..0000000 --- a/example/ios/.xcode.env.local +++ /dev/null @@ -1,2 +0,0 @@ -export NODE_BINARY=/Users/michaelmiller/.nvm/versions/node/v22.14.0/bin/node - diff --git a/example/ios/CourierReactNativeExample.xcodeproj/project.pbxproj b/example/ios/CourierReactNativeExample.xcodeproj/project.pbxproj index c7c6950..f9f99e6 100644 --- a/example/ios/CourierReactNativeExample.xcodeproj/project.pbxproj +++ b/example/ios/CourierReactNativeExample.xcodeproj/project.pbxproj @@ -632,6 +632,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = CourierReactNativeExample/CourierReactNativeExample.entitlements; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 43; DEVELOPMENT_TEAM = 83BJVWGX4Q; ENABLE_BITCODE = NO; @@ -663,6 +664,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = CourierReactNativeExample/CourierReactNativeExample.entitlements; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 43; DEVELOPMENT_TEAM = 83BJVWGX4Q; INFOPLIST_FILE = CourierReactNativeExample/Info.plist; diff --git a/example/ios/Podfile b/example/ios/Podfile index 7a8422f..74d6684 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -5,7 +5,7 @@ require Pod::Executable.execute_command('node', ['-p', {paths: [process.argv[1]]}, )', __dir__]).strip -# Courier React Native requires iOS 13+ +# Courier React Native requires iOS 15+ platform :ios, '15.0' prepare_react_native_project! @@ -61,6 +61,12 @@ target 'CourierReactNativeExample' do :mac_catalyst_enabled => false ) + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' + end + end + # Strip bitcode from all frameworks bitcode_strip_path = `xcrun --find bitcode_strip`.chomp unless bitcode_strip_path.empty? diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ed64292..15abff0 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,11 +1,11 @@ PODS: - boost (1.83.0) - - courier-react-native (5.6.14): - - Courier_iOS (= 5.7.15) + - courier-react-native (5.6.17): + - Courier_iOS (= 5.8.0) - glog - RCT-Folly (= 2022.05.16.00) - React-Core - - Courier_iOS (5.7.15) + - Courier_iOS (5.8.0) - DoubleConversion (1.1.6) - FBLazyVector (0.73.7) - FBReactNativeSpec (0.73.7): @@ -1067,6 +1067,10 @@ PODS: - React-jsi (= 0.73.7) - React-logger (= 0.73.7) - React-perflogger (= 0.73.7) + - RNCAsyncStorage (1.24.0): + - React-Core + - RNCClipboard (1.16.3): + - React-Core - RNGestureHandler (2.16.0): - glog - RCT-Folly (= 2022.05.16.00) @@ -1137,6 +1141,8 @@ DEPENDENCIES: - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" + - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNScreens (from `../node_modules/react-native-screens`) - RNVectorIcons (from `../node_modules/react-native-vector-icons`) @@ -1253,6 +1259,10 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/react/utils" ReactCommon: :path: "../node_modules/react-native/ReactCommon" + RNCAsyncStorage: + :path: "../node_modules/@react-native-async-storage/async-storage" + RNCClipboard: + :path: "../node_modules/@react-native-clipboard/clipboard" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" RNScreens: @@ -1264,8 +1274,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: d3f49c53809116a5d38da093a8aa78bf551aed09 - courier-react-native: b9834c8e4b88d7e28ab5d03d8878375308e5fe51 - Courier_iOS: 1930efadafa3c7fd9c13b695582dfd10b8ba68fd + courier-react-native: ddec7e40d19678aad522b513f2662e6202bb617f + Courier_iOS: 129a2192c66aa0c20332cef153cd70a52051b030 DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 FBLazyVector: 9f533d5a4c75ca77c8ed774aced1a91a0701781e FBReactNativeSpec: 40b791f4a1df779e7e4aa12c000319f4f216d40a @@ -1273,56 +1283,58 @@ SPEC CHECKSUMS: glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 hermes-engine: 39589e9c297d024e90fe68f6830ff86c4e01498a libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - RCT-Folly: cd21f1661364f975ae76b3308167ad66b09f53f5 + RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 RCTRequired: 77f73950d15b8c1a2b48ba5b79020c3003d1c9b5 RCTTypeSafety: ede1e2576424d89471ef553b2aed09fbbcc038e3 React: 2ddb437e599df2f1bffa9b248de2de4cfa0227f0 React-callinvoker: 10fc710367985e525e2d65bcc3a73d536c992aea - React-Codegen: 13401f084876d8785f19964b34846dd06e1959b8 - React-Core: 94539e5a6c01f05f5b3b51207cb2452297a27835 - React-CoreModules: 7e5aacc985835c07faa8e95406e3f7326cce92f1 - React-cxxreact: 67431ad6c2e24ce761814923fecd5f27eef06a9c + React-Codegen: b9dc80301260919cafafb6651cb8dbde889b7090 + React-Core: c771634f2ed0c59bef0bcd3d85d6f2c3af91eb96 + React-CoreModules: 97e5860e7e2d8549a5a357c2e0b115e93f25e5e7 + React-cxxreact: 62fcadb4e0d38175f8f220d9c970c5dbeed2eae4 React-debug: 2bb2ea6d53636bfdc7cb9009a8e71dd6a96810b5 - React-Fabric: 76920f351a7377e6c0562bfbf0e958c9db858ad1 - React-FabricImage: 34e56bb3df3a7007f877d9d60fcf963f1ed8526f - React-graphics: 1546a1457c26f3e449f02c7f29a17f1e337803d5 - React-hermes: f025011c06c241d6cafbe2f465847394551a704c - React-ImageManager: aaf81ba2bb317b6d1287cd71c5b73467a99ea78a - React-jserrorhandler: 895a7b5fbca5bdae900f0e01887e00f15b748cc2 - React-jsi: 580e219940861c5d90156dfc52deb700c6d09d53 - React-jsiexecutor: ed868f628f463ab7409a15dcfd3e38101e2e7f7d + React-Fabric: 0a19152fe4ce3bb38e27ed5072e9d1857631c3fa + React-FabricImage: 1f2a0841508f8c4eef229c84f3f625fcaf00ac0f + React-graphics: 860acbbd1398a1c50d2a0b7fd37ca4f1ae5cef5e + React-hermes: 938f6b4c585b3f98d9b65bfd9b84ebeb29e4db2b + React-ImageManager: b55d5ffffaaa7678bcb5799f95918cb20924d3a8 + React-jserrorhandler: 872045564849dadc0692bf034be4fc77b0f6c3e8 + React-jsi: 5fa3dfbe4f1d6b1fb08650cb877c340ddb70066d + React-jsiexecutor: d7e1aa9e9aefff1e6aee2beea13eb98be431a67f React-jsinspector: f356e49aa086380d3a4892708ca173ad31ac69c1 - React-logger: 3017d7c365f7df9a4575f13e98c1ef1d96b85ba5 - React-Mapbuffer: 768950d2253c4d3da3c40ac7259eafd3ce6f30f2 - react-native-pager-view: e51e09d0f8229b199b24a7b1a7122aae76bd577d - react-native-safe-area-context: 8c70551c8688cd584a53487aa1b9361e991a3b4a - react-native-segmented-control: 5acfc346871654bd2272126159aaaae48f10fbe6 + React-logger: 7b19bdfb254772a0332d6cd4d66eceb0678b6730 + React-Mapbuffer: 6f392912435adb8fbf4c3eee0e79a0a0b4e4b717 + react-native-pager-view: f1e72cb827406f1b49f007ab07400197d421b6e7 + react-native-safe-area-context: dcab599c527c2d7de2d76507a523d20a0b83823d + react-native-segmented-control: b92809e9111013dfa266e1168ba366d62898d9a4 React-nativeconfig: 754233aac2a769578f828093b672b399355582e6 - React-NativeModulesApple: bc05994d236d9c39b64e299ca2154e37a50624a6 + React-NativeModulesApple: a03b2da2b8e127d5f5ee29c683e0deba7a9e1575 React-perflogger: 68ec84e2f858a3e35009aef8866b55893e5e0a1f React-RCTActionSheet: 348c4c729fdfb874f6937abbea90355ecaa6977c - React-RCTAnimation: a2a1329fb302f92fc09a5072790e5ee33f34c599 - React-RCTAppDelegate: 4072a33e6a4318d5ea8342eba4fd753f08a09dc5 - React-RCTBlob: 64279ac7ce41c5c8a5de583d2a2ea08b2f35c9e8 - React-RCTFabric: 40398a34932b3b8e713ec9b9a988b6fbd48957af - React-RCTImage: 647298d9a05f18296ad14deb9faffcccbf90c3fa - React-RCTLinking: 1af4b83559ba537485dc46dba8a7eaa3cb2ceeae - React-RCTNetwork: 99b13f8ff8a3a8aed6f5d9c2295895938c6089ad - React-RCTSettings: da7241ef344854b2c1d84d80d652a0535f304c41 - React-RCTText: 4e24f5a4e5473448c73e60c19ef5e1c48da1e835 - React-RCTVibration: 8a00b2adcae0924f1764a937c0b18291f02857ee - React-rendererdebug: 5bf5b7629e5bc76202c2cdde6098c5e21be45e51 + React-RCTAnimation: 9fb1232af37d25d03415af2e0b8ab3b585ed856d + React-RCTAppDelegate: e0d41ac7fc71b5badb381c70ba585951ba7c8d2a + React-RCTBlob: 70b608915d20ffd397f8ba52278bee7e73f16994 + React-RCTFabric: 8f1fbaba0d9484dab098886b0c2fb7388212073a + React-RCTImage: 520fe02462804655e39b6657996e47277e6f0115 + React-RCTLinking: fb46b9dfea24f4a876163f95769ab279851e0b65 + React-RCTNetwork: dd4396889c20fa8872d4028a4d08f2d2888e2c7f + React-RCTSettings: a7d6fe4b52b98c08b12532a42a18cb12a1667d0a + React-RCTText: df7267a4bc092429fcf285238fbe67a89406ff44 + React-RCTVibration: df03af479dc7ec756e2ca73eb6ce2fa3da6b2888 + React-rendererdebug: ce0744f4121882c76d7a1b2836b8353246d884f8 React-rncore: 80f994ce0ea6bbe84fefebd74f9381636907326c React-runtimeexecutor: b7f307017d54701cf3a4ae41c7558051e0660658 - React-runtimescheduler: 76c7ba9e6c9deb4b1a30a8bae4b7db2930789d5e - React-utils: ef6ed9a5748c207f067ebc9929327d0f3937f671 - ReactCommon: a55ba7975ea32f1a5f808f3e2782e674c439cbf0 - RNGestureHandler: e262eeb792addec0705a116456f210ee1be0dcd0 - RNScreens: 321d9067091167f6255474ea0567f4b4ac09958a - RNVectorIcons: fab5ba3ec74be0cdd3952ca28416214a921722a2 + React-runtimescheduler: a884a55560e2a90caa1cbe0b9eaa24a5add4fa2c + React-utils: d07d009101c7dabff68b710da5c4a47b7a850d98 + ReactCommon: 8cae78d3c3eceff20ee4bbca8bb73b675a45fd5d + RNCAsyncStorage: ec53e44dc3e75b44aa2a9f37618a49c3bc080a7a + RNCClipboard: dfeb43751adff21e588657b5b6c888c72f3aa68e + RNGestureHandler: bc2cdb2dc42facdf34992ae364b8a728e19a3686 + RNScreens: 134a7511b12b8eb440b87aac21e36a71295d6024 + RNVectorIcons: 8b5bb0fa61d54cd2020af4f24a51841ce365c7e9 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: 47d399a73c0c0caa9ff824e5c657eae31215bfee -PODFILE CHECKSUM: 4a0428b44f8e5b4b3b83a3393e142d0fe6af8310 +PODFILE CHECKSUM: 2d425c1f2088237ab87c275fcfa504d283754695 COCOAPODS: 1.16.2 diff --git a/example/metro.config.js b/example/metro.config.js index c243da7..1a5c1f7 100644 --- a/example/metro.config.js +++ b/example/metro.config.js @@ -31,7 +31,7 @@ const config = { acc[name] = path.join(__dirname, 'node_modules', name); return acc; }, {}), - '@trycourier/courier-react-native': path.resolve(__dirname, '..') + '@trycourier/courier-react-native': path.resolve(__dirname, '..'), }, assetExts: ['png', 'jpg', 'jpeg', 'gif', 'webp'], }, @@ -46,4 +46,4 @@ const config = { }, }; -module.exports = mergeConfig(getDefaultConfig(__dirname), config); \ No newline at end of file +module.exports = mergeConfig(getDefaultConfig(__dirname), config); diff --git a/example/package.json b/example/package.json index bb1fbc6..81a5b16 100644 --- a/example/package.json +++ b/example/package.json @@ -10,11 +10,14 @@ "setupEnv": "if [[ ! -e .env ]] ; then cp .env.sample .env ; fi" }, "dependencies": { + "@react-native-async-storage/async-storage": "~1.24.0", + "@react-native-clipboard/clipboard": "^1.16.3", "@react-native-segmented-control/segmented-control": "^2.5.2", "@react-navigation/bottom-tabs": "^6.5.20", "@react-navigation/material-top-tabs": "^6.6.13", "@react-navigation/native": "^6.1.17", "@react-navigation/stack": "^6.3.29", + "@trycourier/courier-react-native": "link:..", "eventemitter3": "^5.0.1", "react": "18.2.0", "react-native": "0.73.7", @@ -23,8 +26,7 @@ "react-native-safe-area-context": "^4.9.0", "react-native-screens": "^3.30.1", "react-native-tab-view": "^3.5.2", - "react-native-vector-icons": "^10.0.0", - "@trycourier/courier-react-native": "link:.." + "react-native-vector-icons": "^10.0.0" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/example/src/App.tsx b/example/src/App.tsx index 3e72729..73cba1d 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -5,7 +5,6 @@ import Toast from 'react-native-toast-message'; import { Poke } from './Poke'; export default function App() { - return ( @@ -14,5 +13,4 @@ export default function App() { ); - } diff --git a/example/src/AuthPreferences.ts b/example/src/AuthPreferences.ts new file mode 100644 index 0000000..cd71387 --- /dev/null +++ b/example/src/AuthPreferences.ts @@ -0,0 +1,72 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { CourierEnvironment, DEFAULT_URLS } from './CourierEnvironment'; +import Env from './Env'; + +const KEYS = { + environment: '@auth_environment', + userId: '@auth_userId', + tenantId: '@auth_tenantId', + apiKey: '@auth_apiKey', + restUrl: '@auth_restUrl', + graphqlUrl: '@auth_graphqlUrl', + inboxGraphqlUrl: '@auth_inboxGraphqlUrl', + inboxWebSocketUrl: '@auth_inboxWebSocketUrl', +} as const; + +export interface AuthPreferencesData { + environment: CourierEnvironment; + userId: string; + tenantId: string; + apiKey: string; + restUrl: string; + graphqlUrl: string; + inboxGraphqlUrl: string; + inboxWebSocketUrl: string; +} + +export async function loadAuthPreferences(): Promise { + const results = await AsyncStorage.getMany([ + KEYS.environment, + KEYS.userId, + KEYS.tenantId, + KEYS.apiKey, + KEYS.restUrl, + KEYS.graphqlUrl, + KEYS.inboxGraphqlUrl, + KEYS.inboxWebSocketUrl, + ]); + + return { + environment: + (results[KEYS.environment] as CourierEnvironment) ?? + CourierEnvironment.Production, + userId: results[KEYS.userId] ?? '', + tenantId: results[KEYS.tenantId] ?? '', + apiKey: results[KEYS.apiKey] ?? Env.authKey, + restUrl: results[KEYS.restUrl] ?? DEFAULT_URLS.rest, + graphqlUrl: results[KEYS.graphqlUrl] ?? DEFAULT_URLS.graphql, + inboxGraphqlUrl: results[KEYS.inboxGraphqlUrl] ?? DEFAULT_URLS.inboxGraphql, + inboxWebSocketUrl: + results[KEYS.inboxWebSocketUrl] ?? DEFAULT_URLS.inboxWebSocket, + }; +} + +export async function saveAuthPreferences( + data: Partial +): Promise { + const entries: Record = {}; + if (data.environment !== undefined) + entries[KEYS.environment] = data.environment; + if (data.userId !== undefined) entries[KEYS.userId] = data.userId; + if (data.tenantId !== undefined) entries[KEYS.tenantId] = data.tenantId; + if (data.apiKey !== undefined) entries[KEYS.apiKey] = data.apiKey; + if (data.restUrl !== undefined) entries[KEYS.restUrl] = data.restUrl; + if (data.graphqlUrl !== undefined) entries[KEYS.graphqlUrl] = data.graphqlUrl; + if (data.inboxGraphqlUrl !== undefined) + entries[KEYS.inboxGraphqlUrl] = data.inboxGraphqlUrl; + if (data.inboxWebSocketUrl !== undefined) + entries[KEYS.inboxWebSocketUrl] = data.inboxWebSocketUrl; + if (Object.keys(entries).length > 0) { + await AsyncStorage.setMany(entries); + } +} diff --git a/example/src/CourierEnvironment.ts b/example/src/CourierEnvironment.ts new file mode 100644 index 0000000..581adc2 --- /dev/null +++ b/example/src/CourierEnvironment.ts @@ -0,0 +1,67 @@ +import { CourierApiUrls } from '@trycourier/courier-react-native'; + +export interface CourierEnvironmentUrls { + rest: string; + graphql: string; + inboxGraphql: string; + inboxWebSocket: string; +} + +export enum CourierEnvironment { + Production = 'Production', + ProductionEU = 'Production EU', + Staging = 'Staging', + Dev = 'Dev', + Custom = 'Custom', +} + +const environmentUrls: Record = { + [CourierEnvironment.Production]: { + rest: 'https://api.courier.com', + graphql: 'https://api.courier.com/client/q', + inboxGraphql: 'https://inbox.courier.com/q', + inboxWebSocket: 'wss://realtime.courier.io', + }, + [CourierEnvironment.ProductionEU]: { + rest: 'https://api.eu.courier.com', + graphql: 'https://api.eu.courier.com/client/q', + inboxGraphql: 'https://inbox.eu.courier.io/q', + inboxWebSocket: 'wss://realtime.eu.courier.io', + }, + [CourierEnvironment.Staging]: { + rest: 'https://api.courierstaging.com', + graphql: 'https://api.courierstaging.com/client/q', + inboxGraphql: 'http://inbox.courierstaging.com/', + inboxWebSocket: + 'wss://inbox-staging-ws-alb-490231599.us-east-1.elb.amazonaws.com', + }, + [CourierEnvironment.Dev]: { + rest: 'https://api.courierdev.com', + graphql: 'https://api.courierdev.com/client/q', + inboxGraphql: 'https://inbox.courierdev.com/q', + inboxWebSocket: 'wss://9mrugsdnk1.execute-api.us-east-1.amazonaws.com/dev', + }, +}; + +export const DEFAULT_URLS: CourierEnvironmentUrls = { + rest: 'https://api.courier.com', + graphql: 'https://api.courier.com/client/q', + inboxGraphql: 'https://inbox.courier.com/q', + inboxWebSocket: 'wss://realtime.courier.io', +}; + +export function getUrlsForEnvironment( + env: CourierEnvironment +): CourierEnvironmentUrls | null { + if (env === CourierEnvironment.Custom) return null; + return environmentUrls[env] ?? null; +} + +export function toApiUrls(urls: CourierEnvironmentUrls): CourierApiUrls { + return { + rest: urls.rest, + graphql: urls.graphql, + inboxGraphql: urls.inboxGraphql, + inboxWebSocket: urls.inboxWebSocket, + }; +} diff --git a/example/src/Emitter.tsx b/example/src/Emitter.tsx index c1172ba..1a783dd 100644 --- a/example/src/Emitter.tsx +++ b/example/src/Emitter.tsx @@ -6,10 +6,16 @@ export function emitEvent(eventName: string, eventData?: any): void { eventEmitter.emit(eventName, eventData); } -export function addListener(eventName: string, listener: (...args: any[]) => void): void { +export function addListener( + eventName: string, + listener: (...args: any[]) => void +): void { eventEmitter.on(eventName, listener); } -export function removeListener(eventName: string, listener: (...args: any[]) => void): void { +export function removeListener( + eventName: string, + listener: (...args: any[]) => void +): void { eventEmitter.off(eventName, listener); -} \ No newline at end of file +} diff --git a/example/src/Home.tsx b/example/src/Home.tsx index 58b2cb0..b408b25 100644 --- a/example/src/Home.tsx +++ b/example/src/Home.tsx @@ -1,5 +1,8 @@ import React, { useEffect, useState } from 'react'; -import { BottomTabNavigationOptions, createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { + BottomTabNavigationOptions, + createBottomTabNavigator, +} from '@react-navigation/bottom-tabs'; import { Alert, Button } from 'react-native'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import Auth from './pages/Auth'; @@ -12,14 +15,15 @@ import Tests from './pages/Tests'; const Tab = createBottomTabNavigator(); const Home = () => { - const [unreadCount, setUnreadCount] = useState(0); useEffect(() => { const setupInbox = async () => { // Setup Push - Courier.setIOSForegroundPresentationOptions({ options: ['sound', 'badge', 'list', 'banner'] }); + Courier.setIOSForegroundPresentationOptions({ + options: ['sound', 'badge', 'list', 'banner'], + }); const pushListener = Courier.shared.addPushNotificationListener({ onPushNotificationClicked(push) { @@ -29,7 +33,7 @@ const Home = () => { onPushNotificationDelivered(push) { console.log(push); Alert.alert('📬 Push Notification Delivered', JSON.stringify(push)); - } + }, }); // Setup Inbox @@ -53,14 +57,12 @@ const Home = () => { inboxListener.remove(); }); }; - }, []); const inboxOptions = (): BottomTabNavigationOptions => { - const badgeCount = () => { return unreadCount > 0 ? unreadCount : undefined; - } + }; return { headerRight: () => ( @@ -72,24 +74,36 @@ const Home = () => { tabBarBadge: badgeCount(), tabBarIcon: ({ color, size }) => ( - ) - } - } + ), + }; + }; const icon = (icon: string): BottomTabNavigationOptions => { return { tabBarIcon: ({ color, size }) => ( - ) - } - } + ), + }; + }; return ( - - + + - + ); diff --git a/example/src/Poke.tsx b/example/src/Poke.tsx index dc8f25e..8603742 100644 --- a/example/src/Poke.tsx +++ b/example/src/Poke.tsx @@ -1,4 +1,10 @@ -import React, { useState, useEffect, useMemo, createContext, useContext } from 'react'; +import React, { + useState, + useEffect, + useMemo, + createContext, + useContext, +} from 'react'; import { View, StyleSheet, ViewStyle, Animated, Easing } from 'react-native'; interface Touch { @@ -44,14 +50,19 @@ export const Poke: React.FC = ({ initialTouchTimeout = 150, }) => { const [enabled, setEnabled] = useState(initialEnabled); - const [indicatorStyle, setIndicatorStyle] = useState(initialIndicatorStyle || {}); + const [indicatorStyle, setIndicatorStyle] = useState( + initialIndicatorStyle || {} + ); const [touchTimeout, setTouchTimeout] = useState(initialTouchTimeout); - const contextValue = useMemo(() => ({ - setEnabled, - setIndicatorStyle, - setTouchTimeout, - }), []); + const contextValue = useMemo( + () => ({ + setEnabled, + setIndicatorStyle, + setTouchTimeout, + }), + [] + ); return ( @@ -73,11 +84,11 @@ interface TouchIndicatorProps { touchTimeout: number; } -const TouchIndicator: React.FC = ({ - children, - indicatorStyle, - enabled, - touchTimeout +const TouchIndicator: React.FC = ({ + children, + indicatorStyle, + enabled, + touchTimeout, }) => { const [latestTouch, setLatestTouch] = useState(null); @@ -118,18 +129,22 @@ const TouchIndicator: React.FC = ({ } }; - const touchIndicatorStyle = useMemo(() => ({ - position: 'absolute', - width: indicatorSize, - height: indicatorSize, - borderRadius: indicatorSize / 2, - backgroundColor: indicatorColor, - zIndex: 9999, - } as ViewStyle), [indicatorSize, indicatorColor]); + const touchIndicatorStyle = useMemo( + () => + ({ + position: 'absolute', + width: indicatorSize, + height: indicatorSize, + borderRadius: indicatorSize / 2, + backgroundColor: indicatorColor, + zIndex: 9999, + } as ViewStyle), + [indicatorSize, indicatorColor] + ); return ( - void }) => ( - - {title} +export const Tab = ({ + title, + isSelected, + onPress, +}: { + title: string; + isSelected: boolean; + onPress: () => void; +}) => ( + + + {title} + ); -export const TabControl = ({ tabs, selectedTab, setSelectedTab }: { tabs: TabItem[]; selectedTab: string; setSelectedTab: (tab: string) => void }) => ( +export const TabControl = ({ + tabs, + selectedTab, + setSelectedTab, +}: { + tabs: TabItem[]; + selectedTab: string; + setSelectedTab: (tab: string) => void; +}) => ( {tabs.map((tab) => ( { - + public static async generateJwt(props: { + authKey: string; + userId: string; + baseUrl?: string; + }): Promise { return new Promise((resolve, reject) => { - - const url = 'https://api.courier.com/auth/issue-token'; + const base = props.baseUrl ?? 'https://api.courier.com'; + const url = `${base}/auth/issue-token`; const request = { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${props.authKey}` + 'Authorization': `Bearer ${props.authKey}`, }, body: JSON.stringify({ scope: `user_id:${props.userId} write:user-tokens inbox:read:messages inbox:write:events read:preferences write:preferences read:brands`, - expires_in: '2 days' - }) + expires_in: '2 days', + }), }; fetch(url, request) - .then(response => response.json()) + .then((response) => response.json()) .then((data: Response) => { resolve(data.token); }) - .catch(error => { + .catch((error) => { reject(error); }); - }); - } - public static async sendTest(props: { authKey: string, userId: string, channel: string, title?: string, body?: string }): Promise { - const url = 'https://api.courier.com/send'; + public static async sendTest(props: { + authKey: string; + userId: string; + channel: string; + title?: string; + body?: string; + baseUrl?: string; + }): Promise { + const base = props.baseUrl ?? 'https://api.courier.com'; + const url = `${base}/send`; const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${props.authKey}`, }; const body = JSON.stringify({ - 'message': { - 'to': { - 'user_id': props.userId + message: { + to: { + user_id: props.userId, }, - 'content': { - 'title': props.title ?? 'Test', - 'body': props.body ?? 'Body', + content: { + title: props.title ?? 'Test', + body: props.body ?? 'Body', }, - 'routing': { - 'method': 'all', - 'channels': [props.channel], + routing: { + method: 'all', + channels: [props.channel], }, }, }); @@ -64,24 +72,22 @@ export class ExampleServer { if (response.status === 202) { const json = await response.json(); - return json['requestId'] ?? 'Error'; + return json.requestId ?? 'Error'; } else { throw new Error('Failed to send test message'); } } - } export class Utils { - static generateUUID(): string { let uuid = ''; - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const characters = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const charactersLength = characters.length; for (let i = 0; i < 16; i++) { uuid += characters.charAt(Math.floor(Math.random() * charactersLength)); } return uuid; } - -} \ No newline at end of file +} diff --git a/example/src/pages/Auth.tsx b/example/src/pages/Auth.tsx index c9ac9f2..25e7bc8 100644 --- a/example/src/pages/Auth.tsx +++ b/example/src/pages/Auth.tsx @@ -1,311 +1,509 @@ -import Courier from "@trycourier/courier-react-native"; -import React, { useEffect, useRef, useState } from "react"; -import { ActivityIndicator, Button, Modal, Platform, StyleSheet, Text, TextInput, TouchableOpacity, View } from "react-native"; -import Env from "../Env"; -import { ExampleServer } from "../Utils"; -import { usePoke } from '../Poke'; +import Courier from '@trycourier/courier-react-native'; +import React, { useCallback, useEffect, useState } from 'react'; +import { + ActivityIndicator, + Alert, + FlatList, + Modal, + Platform, + Pressable, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; +import Clipboard from '@react-native-clipboard/clipboard'; +import { ExampleServer } from '../Utils'; +import { + CourierEnvironment, + getUrlsForEnvironment, + toApiUrls, +} from '../CourierEnvironment'; +import { + AuthPreferencesData, + loadAuthPreferences, + saveAuthPreferences, +} from '../AuthPreferences'; + +const MONO_FONT = Platform.select({ + ios: 'Courier', + android: 'monospace', + default: 'monospace', +}); + +interface OptionRow { + key: string; + value: string; +} const Auth = () => { - - const [isLoading, setIsLoading] = useState(false) - const [userId, setUserId] = useState() - const [tenantId, setTenantId] = useState() + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [options, setOptions] = useState([]); + const [selectedEnvironment, setSelectedEnvironment] = + useState(CourierEnvironment.Production); + const [envPickerVisible, setEnvPickerVisible] = useState(false); + const [editModalVisible, setEditModalVisible] = useState(false); + const [editIndex, setEditIndex] = useState(-1); + const [editValue, setEditValue] = useState(''); + + const buildOptions = useCallback( + (prefs: AuthPreferencesData): OptionRow[] => { + return [ + { key: 'Environment', value: prefs.environment }, + { key: 'User ID', value: prefs.userId }, + { key: 'Tenant ID (Optional)', value: prefs.tenantId }, + { key: 'API Key', value: prefs.apiKey }, + { key: 'REST URL', value: prefs.restUrl }, + { key: 'GraphQL URL', value: prefs.graphqlUrl }, + { key: 'Inbox GraphQL URL', value: prefs.inboxGraphqlUrl }, + { key: 'Inbox WebSocket', value: prefs.inboxWebSocketUrl }, + ]; + }, + [] + ); useEffect(() => { - const initAuth = async () => { - const authListener = await Courier.shared.addAuthenticationListener({ - onUserChanged: async (userId) => { - setUserId(userId); - setTenantId(await Courier.shared.getTenantId()); - console.log(`User changed: ${userId}`); - console.log(`Tenant changed: ${await Courier.shared.getTenantId()}`); - } - }); + let authListener: any; - const userId = await Courier.shared.getUserId(); - const tenantId = await Courier.shared.getTenantId(); - console.log(`Initial user: ${userId}`); - console.log(`Initial tenant: ${tenantId}`); - refreshJWT(userId, tenantId); + const init = async () => { + const prefs = await loadAuthPreferences(); + setSelectedEnvironment(prefs.environment); + setOptions(buildOptions(prefs)); + setIsLoading(false); - return authListener; + authListener = await Courier.shared.addAuthenticationListener({ + onUserChanged: async (_userId) => { + // Refresh UI state when auth changes externally + }, + }); }; - let listener: any; - initAuth().then(result => { - listener = result; - }); + init(); return () => { - if (listener) { - listener.remove(); - } + authListener?.remove(); }; + }, [buildOptions]); - }, []); - - async function refreshJWT(userId?: string, tenantId?: string) { - - console.log('Refreshing JWT'); - - if (!userId) { - console.log(`No user found`); - setIsLoading(false); - return; - } - - setIsLoading(true); + const saveEnabled = options.length > 1 && options[1]!.value.length > 0; + const performSave = async () => { + setIsSaving(true); try { - - console.log(`User ID: ${userId}`); - - await Courier.shared.signOut(); - - const token = await ExampleServer.generateJwt({ - authKey: Env.authKey, - userId: userId, - }); - - console.log(`New token: ${token}`); - - await Courier.shared.signIn({ - accessToken: token, - userId: userId, - tenantId: tenantId, - }); - - } catch (e) { - - console.error(e); - + if (await Courier.shared.getUserId()) { + await Courier.shared.signOut(); + } + await performSignIn(); + } catch (e: any) { + Alert.alert('Error', e?.message ?? String(e)); } + setIsSaving(false); + }; - setIsLoading(false); - - } - - async function signIn(userId: string, tenantId: string) { - - console.log('Signing User In'); - console.log(`User ID: ${userId}`); - console.log(`Tenant ID: ${tenantId}`); - - setIsLoading(true); - - try { - - const token = await ExampleServer.generateJwt({ - authKey: Env.authKey, - userId: userId, - }); - - console.log(`New token: ${token}`); - - await Courier.shared.signIn({ - accessToken: token, - userId: userId, - tenantId: tenantId.length ? tenantId : undefined - }); - - setUserId(await Courier.shared.getUserId()); - setTenantId(await Courier.shared.getTenantId()); - - } catch (e) { - - console.error(e); + const performSignIn = async () => { + const userId = options[1]!.value; + const tenantId = options[2]!.value || undefined; + const apiKey = options[3]!.value; + const restUrl = options[4]!.value; + if (!userId) { + await Courier.shared.signOut(); + return; } - setIsLoading(false); - - } - - async function signOut() { - await Courier.shared.signOut(); - setUserId(await Courier.shared.getUserId()); - setTenantId(await Courier.shared.getTenantId()); - } + const jwt = await ExampleServer.generateJwt({ + authKey: apiKey, + userId: userId, + baseUrl: restUrl, + }); - const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - text: { - marginBottom: 10, - fontFamily: Platform.select({ - ios: 'Courier', - android: 'monospace', - default: 'monospace', + await Courier.shared.signIn({ + userId: userId, + tenantId: tenantId, + accessToken: jwt, + apiUrls: toApiUrls({ + rest: options[4]!.value, + graphql: options[5]!.value, + inboxGraphql: options[6]!.value, + inboxWebSocket: options[7]!.value, }), - fontSize: 16, - }, - }); + }); + }; - const AuthButton = (props: { buttonText: string }) => { + const onRowTapped = (index: number) => { + if (index === 0) { + setEnvPickerVisible(true); + } else if ( + index >= 4 && + selectedEnvironment !== CourierEnvironment.Custom + ) { + // URL rows in non-custom mode: copy to clipboard + const val = options[index]!.value; + if (val) { + Clipboard.setString(val); + Alert.alert('Copied', val); + } + } else { + setEditIndex(index); + setEditValue(options[index]!.value); + setEditModalVisible(true); + } + }; - const [modalVisible, setModalVisible] = useState(false); - const [userId, setUserId] = useState(''); - const [tenantId, setTenantId] = useState(''); - const inputRef = useRef(null); + const applyEnvironment = async (env: CourierEnvironment) => { + setSelectedEnvironment(env); + const urls = getUrlsForEnvironment(env) ?? { + rest: options[4]!.value, + graphql: options[5]!.value, + inboxGraphql: options[6]!.value, + inboxWebSocket: options[7]!.value, + }; - const styles = StyleSheet.create({ - button: { - backgroundColor: 'lightgray', - padding: 10, - borderRadius: 5, - }, - buttonText: { - fontSize: 16, - fontFamily: Platform.select({ - ios: 'Courier', - android: 'monospace', - default: 'monospace', - }), - }, - modalContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.5)', - }, - modalContent: { - backgroundColor: 'white', - padding: 20, - borderRadius: 5, - elevation: 5, - minWidth: 300, - }, - input: { - borderWidth: 1, - borderColor: 'gray', - borderRadius: 5, - padding: 10, - marginBottom: 10, - }, + const updated = [...options]; + updated[0] = { key: 'Environment', value: env }; + updated[4] = { key: 'REST URL', value: urls.rest }; + updated[5] = { key: 'GraphQL URL', value: urls.graphql }; + updated[6] = { key: 'Inbox GraphQL URL', value: urls.inboxGraphql }; + updated[7] = { key: 'Inbox WebSocket', value: urls.inboxWebSocket }; + setOptions(updated); + + await saveAuthPreferences({ + environment: env, + restUrl: urls.rest, + graphqlUrl: urls.graphql, + inboxGraphqlUrl: urls.inboxGraphql, + inboxWebSocketUrl: urls.inboxWebSocket, }); + }; - useEffect(() => { - if (modalVisible) { - inputRef.current.focus(); - } - }, [modalVisible]); - - const handleButtonPress = async () => { - - if (await Courier.shared.getUserId()) { - await signOut(); - } else { - setModalVisible(true); - } + const handleEditSave = async () => { + const updated = [...options]; + updated[editIndex] = { ...updated[editIndex]!, value: editValue }; - }; - - const handleModalClose = () => { - setModalVisible(false); - }; - - const handleUserIdInputChange = (text: string) => { - setUserId(text); - }; + // If editing a URL row, switch to Custom env + if (editIndex >= 4 && selectedEnvironment !== CourierEnvironment.Custom) { + setSelectedEnvironment(CourierEnvironment.Custom); + updated[0] = { key: 'Environment', value: CourierEnvironment.Custom }; + await saveAuthPreferences({ environment: CourierEnvironment.Custom }); + } - const handleTenantIdInputChange = (text: string) => { - setTenantId(text); - }; - - const handleSaveButtonPress = () => { - signIn(userId, tenantId); - setModalVisible(false); + setOptions(updated); + setEditModalVisible(false); + + const prefKeyMap: Record = { + 1: 'userId', + 2: 'tenantId', + 3: 'apiKey', + 4: 'restUrl', + 5: 'graphqlUrl', + 6: 'inboxGraphqlUrl', + 7: 'inboxWebSocketUrl', }; - - return ( - <> - - - - Set Courier User Id: - - Set Tenant Id: - -