diff --git a/.claude/skills/spec-from-audit/SKILL.md b/.claude/skills/spec-from-audit/SKILL.md index 1d8ff769e..40f6cdec4 100644 --- a/.claude/skills/spec-from-audit/SKILL.md +++ b/.claude/skills/spec-from-audit/SKILL.md @@ -101,7 +101,7 @@ Follow these strictly: 1. **Decisions, not options** — "Use local wrappers" not "Consider adding to Euclid or using local wrappers." Every ambiguous implementation choice must be resolved in the spec. If you genuinely can't decide, flag it as a blocker and ask the user — don't embed it as an option. 2. **Second person** — "You are fixing...", "You will modify..." -3. **Exact file paths with line numbers** — `src/utils/sumsubProvider.ts:118`, not "the provider file" +3. **Exact file paths with line numbers** — `src/utils/kycProvider.ts:118`, not "the provider file" 4. **Current code, not stale references** — you read the files in Step 2, use what you actually saw 5. **Explicit constraints** — "You will NOT modify..." sections prevent scope creep 6. **Required vs optional** — mark every item. Don't let agents infer priority. diff --git a/.github/workflows/mobile-deploy.yml b/.github/workflows/mobile-deploy.yml index e17f4c2cf..6b17ae02b 100644 --- a/.github/workflows/mobile-deploy.yml +++ b/.github/workflows/mobile-deploy.yml @@ -715,7 +715,7 @@ jobs: SEGMENT_KEY: ${{ secrets.SEGMENT_KEY }} SELFXYZ_APP_TOKEN: ${{ steps.github-token.outputs.token }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - DIDIT_TEE_URL: ${{ secrets.DIDIT_TEE_URL }} + KYC_TEE_URL: ${{ secrets.KYC_TEE_URL }} TURNKEY_AUTH_PROXY_CONFIG_ID: ${{ secrets.TURNKEY_AUTH_PROXY_CONFIG_ID }} TURNKEY_GOOGLE_CLIENT_ID: ${{ secrets.TURNKEY_GOOGLE_CLIENT_ID }} TURNKEY_ORGANIZATION_ID: ${{ secrets.TURNKEY_ORGANIZATION_ID }} @@ -1176,7 +1176,7 @@ jobs: NODE_OPTIONS: "--max-old-space-size=6144" SEGMENT_KEY: ${{ secrets.SEGMENT_KEY }} SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - DIDIT_TEE_URL: ${{ secrets.DIDIT_TEE_URL }} + KYC_TEE_URL: ${{ secrets.KYC_TEE_URL }} TURNKEY_AUTH_PROXY_CONFIG_ID: ${{ secrets.TURNKEY_AUTH_PROXY_CONFIG_ID }} TURNKEY_GOOGLE_CLIENT_ID: ${{ secrets.TURNKEY_GOOGLE_CLIENT_ID }} TURNKEY_ORGANIZATION_ID: ${{ secrets.TURNKEY_ORGANIZATION_ID }} diff --git a/.github/workflows/mobile-e2e.yml b/.github/workflows/mobile-e2e.yml index a6b9640fd..05b17a4a4 100644 --- a/.github/workflows/mobile-e2e.yml +++ b/.github/workflows/mobile-e2e.yml @@ -824,6 +824,12 @@ jobs: SIMULATOR_ID="${IOS_SIMULATOR_ID:-iPhone SE (3rd generation)}" echo "Installing on simulator: $SIMULATOR_ID" + echo "Erasing simulator to ensure clean state..." + xcrun simctl shutdown "$SIMULATOR_ID" 2>/dev/null || true + xcrun simctl erase "$SIMULATOR_ID" + xcrun simctl boot "$SIMULATOR_ID" || true + xcrun simctl bootstatus "$SIMULATOR_ID" -b + echo "Removing any existing app installation..." xcrun simctl uninstall "$SIMULATOR_ID" "$IOS_BUNDLE_ID" 2>/dev/null || true diff --git a/.gitignore b/.gitignore index 6f3e3d1a1..9749eec14 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ package-lock.json !.claude/skills/ **/.claude/settings.json **/.claude/settings.local.json +.mcp.json # CI-generated tarballs (don't commit these!) mobile-sdk-alpha-ci.tgz diff --git a/CLAUDE.md b/CLAUDE.md index 66758b0f8..38dbb7f9f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -73,7 +73,7 @@ Specs are agent-executable prompts. A new Claude Code session with no prior cont - **Make decisions, not options.** "Use local wrappers" not "Consider adding to Euclid or using local wrappers." Agents can't choose between approaches — tell them which one. - **Use second person.** "You are fixing X" not "X should be fixed." - **Be explicit about constraints.** "You will NOT modify..." not just "Focus on..." -- **Provide exact file paths with line numbers.** `src/utils/sumsubProvider.ts:118` not "the provider file." +- **Provide exact file paths with line numbers.** `src/utils/kycProvider.ts:118` not "the provider file." - **State the validation command.** Agents will run it. If it's not there, they'll skip validation. - **One spec = one PR.** Target the PR size from Key Rules (1k–3k LOC). If a spec would exceed that, split it. - **Mark items as required vs optional.** Don't let agents infer priority. diff --git a/app/android/app/src/debug/AndroidManifest.xml b/app/android/app/src/debug/AndroidManifest.xml index c8db40b13..bc6dbce28 100644 --- a/app/android/app/src/debug/AndroidManifest.xml +++ b/app/android/app/src/debug/AndroidManifest.xml @@ -14,7 +14,7 @@ tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic"> - + - + - + 11.11.0) - Firebase/CoreOnly (11.11.0): - FirebaseCore (~> 11.11.0) - Firebase/Messaging (11.11.0): @@ -69,6 +74,24 @@ PODS: - FirebaseRemoteConfig (~> 11.11.0) - FirebaseABTesting (11.11.0): - FirebaseCore (~> 11.11.0) + - FirebaseAnalytics (11.11.0): + - FirebaseAnalytics/AdIdSupport (= 11.11.0) + - FirebaseCore (~> 11.11.0) + - FirebaseInstallations (~> 11.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - FirebaseAnalytics/AdIdSupport (11.11.0): + - FirebaseCore (~> 11.11.0) + - FirebaseInstallations (~> 11.0) + - GoogleAppMeasurement (= 11.11.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) - FirebaseCore (11.11.0): - FirebaseCoreInternal (~> 11.11.0) - GoogleUtilities/Environment (~> 8.0) @@ -103,6 +126,26 @@ PODS: - FirebaseSharedSwift (11.15.0) - fmt (11.0.2) - glog (0.3.5) + - GoogleAppMeasurement (11.11.0): + - GoogleAppMeasurement/AdIdSupport (= 11.11.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/AdIdSupport (11.11.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 11.11.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/WithoutAdIdSupport (11.11.0): + - GoogleUtilities/AppDelegateSwizzler (~> 8.0) + - GoogleUtilities/MethodSwizzler (~> 8.0) + - GoogleUtilities/Network (~> 8.0) + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - nanopb (~> 3.30910.0) - GoogleDataTransport (10.1.0): - nanopb (~> 3.30910.0) - PromisesObjC (~> 2.4) @@ -121,6 +164,9 @@ PODS: - GoogleUtilities/Logger (8.1.0): - GoogleUtilities/Environment - GoogleUtilities/Privacy + - GoogleUtilities/MethodSwizzler (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy - GoogleUtilities/Network (8.1.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" @@ -142,12 +188,12 @@ PODS: - hermes-engine (0.77.0): - hermes-engine/Pre-built (= 0.77.0) - hermes-engine/Pre-built (0.77.0) - - lottie-ios (4.5.0) - - lottie-react-native (7.2.2): + - lottie-ios (4.6.0) + - lottie-react-native (7.3.6): - DoubleConversion - glog - hermes-engine - - lottie-ios (= 4.5.0) + - lottie-ios (= 4.6.0) - RCT-Folly (= 2024.11.18.00) - RCTRequired - RCTTypeSafety @@ -1428,7 +1474,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-compat (2.23.1): + - react-native-compat (2.23.9): - DoubleConversion - glog - hermes-engine @@ -1449,13 +1495,14 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - YttriumWrapper (= 0.10.50) - react-native-get-random-values (1.11.0): - React-Core - react-native-netinfo (11.4.1): - React-Core - react-native-nfc-manager (3.17.2): - React-Core - - react-native-passkey (3.3.2): + - react-native-passkey (3.3.3): - DoubleConversion - glog - hermes-engine @@ -1476,7 +1523,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-safe-area-context (5.6.2): + - react-native-safe-area-context (5.7.0): - DoubleConversion - glog - hermes-engine @@ -1489,8 +1536,8 @@ PODS: - React-featureflags - React-graphics - React-ImageManager - - react-native-safe-area-context/common (= 5.6.2) - - react-native-safe-area-context/fabric (= 5.6.2) + - react-native-safe-area-context/common (= 5.7.0) + - react-native-safe-area-context/fabric (= 5.7.0) - React-NativeModulesApple - React-RCTFabric - React-rendererdebug @@ -1499,7 +1546,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-safe-area-context/common (5.6.2): + - react-native-safe-area-context/common (5.7.0): - DoubleConversion - glog - hermes-engine @@ -1520,7 +1567,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-safe-area-context/fabric (5.6.2): + - react-native-safe-area-context/fabric (5.7.0): - DoubleConversion - glog - hermes-engine @@ -1900,6 +1947,10 @@ PODS: - Yoga - RNDeviceInfo (15.0.2): - React-Core + - RNFBAnalytics (21.14.0): + - Firebase/Analytics (= 11.11.0) + - React-Core + - RNFBApp - RNFBApp (21.14.0): - Firebase/CoreOnly (= 11.11.0) - React-Core @@ -1955,7 +2006,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNInAppBrowser (3.7.0): + - RNInAppBrowser (3.7.1): - React-Core - RNKeychain (10.0.0): - DoubleConversion @@ -2131,7 +2182,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - SdkReactNative (3.2.7): + - SdkReactNative (3.2.8): - DiditSDK (~> 3.2) - DoubleConversion - glog @@ -2153,7 +2204,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - segment-analytics-react-native (2.21.4): + - segment-analytics-react-native (2.22.0): - React-Core - sovran-react-native - SelfNFCPassportReader (2.1.1): @@ -2166,6 +2217,7 @@ PODS: - SwiftQRScanner (1.1.6) - SwiftyTesseract (3.1.3) - Yoga (0.0.0) + - YttriumWrapper (0.10.50) DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) @@ -2267,6 +2319,7 @@ DEPENDENCIES: - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) + - "RNFBAnalytics (from `../node_modules/@react-native-firebase/analytics`)" - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" - "RNFBMessaging (from `../node_modules/@react-native-firebase/messaging`)" - "RNFBRemoteConfig (from `../node_modules/@react-native-firebase/remote-config`)" @@ -2292,6 +2345,7 @@ SPEC REPOS: - AppCheckCore - Firebase - FirebaseABTesting + - FirebaseAnalytics - FirebaseCore - FirebaseCoreExtension - FirebaseCoreInternal @@ -2300,6 +2354,7 @@ SPEC REPOS: - FirebaseRemoteConfig - FirebaseRemoteConfigInterop - FirebaseSharedSwift + - GoogleAppMeasurement - GoogleDataTransport - GoogleSignIn - GoogleUtilities @@ -2315,6 +2370,7 @@ SPEC REPOS: - Sentry - SocketRocket - SwiftyTesseract + - YttriumWrapper EXTERNAL SOURCES: boost: @@ -2504,6 +2560,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-clipboard/clipboard" RNDeviceInfo: :path: "../node_modules/react-native-device-info" + RNFBAnalytics: + :path: "../node_modules/@react-native-firebase/analytics" RNFBApp: :path: "../node_modules/@react-native-firebase/app" RNFBMessaging: @@ -2570,6 +2628,7 @@ SPEC CHECKSUMS: FBLazyVector: 2bc03a5cf64e29c611bbc5d7eb9d9f7431f37ee6 Firebase: 6a8f201c61eda24e98f1ce2b44b1b9c2caf525cc FirebaseABTesting: 8551c24eb28e300ce697f8eb72c1a519bb96eb40 + FirebaseAnalytics: acfa848bf81e1a4dbf60ef1f0eddd7328fe6673e FirebaseCore: 2321536f9c423b1f857e047a82b8a42abc6d9e2c FirebaseCoreExtension: 3a64994969dd05f4bcb7e6896c654eded238e75b FirebaseCoreInternal: 31ee350d87b30a9349e907f84bf49ef8e6791e5a @@ -2580,14 +2639,15 @@ SPEC CHECKSUMS: FirebaseSharedSwift: e17c654ef1f1a616b0b33054e663ad1035c8fd40 fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8 + GoogleAppMeasurement: 8a82b93a6400c8e6551c0bcd66a9177f2e067aed GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleSignIn: fcee2257188d5eda57a5e2b6a715550ffff9206d GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238 GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 hermes-engine: 1f783c3d53940aed0d2c84586f0b7a85ab7827ef - lottie-ios: a881093fab623c467d3bce374367755c272bdd59 - lottie-react-native: 6cb05b7b4ea463afe657e3b46784f067858e1a5d + lottie-ios: 8f959969761e9c45d70353667d00af0e5b9cadb3 + lottie-react-native: d73a798e26348851f0ef349df3d40f2e27fd239b Mixpanel-swift: e9bef28a9648faff384d5ba6f48ecc2787eb24c0 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 OpenSSL-Universal: 84efb8a29841f2764ac5403e0c4119a28b713346 @@ -2627,12 +2687,12 @@ SPEC CHECKSUMS: react-native-biometrics: 43ed5b828646a7862dbc7945556446be00798e7d react-native-blur: 745703f35133ed6a1210d4bbff358a631911f002 react-native-cloud-storage: 796c793dc354bb49f9df27ca25eed0f79a15549e - react-native-compat: 10b5f906b469268eaceca83ea2393c177f1ce18a + react-native-compat: ad6a412f03632d1c4d97d47e56b22d0597116085 react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187 react-native-nfc-manager: c8891e460b4943b695d63f7f4effc6345bbefc83 - react-native-passkey: 8818f842d1b80e45c06e906a5c85964719782bf5 - react-native-safe-area-context: 5b5d3eb6ec9ef848f16c064a4eab4a92c7d7895e + react-native-passkey: 3c07a93dc2608929d794b7298c0d29d01b379f01 + react-native-safe-area-context: bf457bf5b3a617e9a3930d1ecd954a3335303cc7 react-native-sqlite-storage: 0c84826214baaa498796c7e46a5ccc9a82e114ed react-native-webview: 05734d99f1e422c5ddfeefbd083d53abd78fccb1 React-nativeconfig: 334c9961d74ddd3bc203afb92ee574ed01c7c755 @@ -2668,20 +2728,21 @@ SPEC CHECKSUMS: RNCAsyncStorage: 6a8127b6987dc9fbce778669b252b14c8355c7ce RNCClipboard: 9f7b908de4bf4353871fb454c15fc03db4917b88 RNDeviceInfo: 4c852998208b60dc192ae3529e5867817719ad1e - RNFBApp: 4105e54d9ca4a1c10893a032268470f670181110 - RNFBMessaging: 6857871d9dff8f26b0c325fc7d97ba69cb77d213 - RNFBRemoteConfig: 8d3675f18c052483ce294bb97b857428467fb41e + RNFBAnalytics: 03c83ba4617a3754c99e66267983efcc908932a9 + RNFBApp: a448037d2df74af9d374a0b765be12ff1e844dc0 + RNFBMessaging: 0f0498a95c605e3afcf13ac5f349d0b201ea65f6 + RNFBRemoteConfig: 4eb5fc9f21dc324153c7d3f5b48c935ab9031876 RNGestureHandler: 36aca36e4ef19f55dbf97239199d00fd58494e34 RNGoogleSignin: 60c3f470558dbff0ae54f2f164ef82a89d3eb561 - RNInAppBrowser: 6d3eb68d471b9834335c664704719b8be1bfdb20 + RNInAppBrowser: 904d24dc75e8e6c6c98a3160329192608946f9df RNKeychain: 35beaa17938f7d8e4990d8a38fad5f8a748fc47c RNLocalize: aa57bee9fcd545b98ce773a8e2404f9a36115b4a RNReactNativeHapticFeedback: eb5395b503c7a8f10de5e6722ef8afd3c61bc4f5 RNScreens: b0811b109e1a0b8b579f3348018e177bee374840 RNSentry: 98ab9f6a16c9596e36565ccf1a5871323f334766 RNSVG: d926926b169d8b81eb06aeb69734076e1dd566a3 - SdkReactNative: 7f65ca10a978bf9440730a537d54511e68ed50b4 - segment-analytics-react-native: 0eae155b0e9fa560fa6b17d78941df64537c35b7 + SdkReactNative: 34ba85b3f3060892c548b7415b06f0bd66fcba1c + segment-analytics-react-native: 8ab9c49df1859bbd6be93cf90a91ade17f20a0aa SelfNFCPassportReader: 8b53f9d483e0dd1f1a275953e3dc6dfc733694c5 Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 @@ -2689,7 +2750,8 @@ SPEC CHECKSUMS: SwiftQRScanner: e85a25f9b843e9231dab89a96e441472fe54a724 SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb Yoga: c34725819ab0a5962e85455b9e56679b306910ee + YttriumWrapper: d7f63336830536f1da41b745ed8bacedb04228c4 -PODFILE CHECKSUM: 0b99fae2ec87b0be3e6d9b3fd87360fe0a84b25f +PODFILE CHECKSUM: 83f631d1b6308502a035e656b2f9dfab30431ae3 COCOAPODS: 1.16.2 diff --git a/app/package.json b/app/package.json index 381e562e0..dde9f9780 100644 --- a/app/package.json +++ b/app/package.json @@ -75,11 +75,6 @@ "web:build": "yarn build:deps && vite build", "web:preview": "vite preview" }, - "resolutions": { - "punycode": "npm:punycode.js@2.3.1", - "react-native-blur-effect": "1.1.3", - "react-native-webview": "13.16.0" - }, "overrides": { "punycode": "npm:punycode.js@2.3.1", "react-native-blur-effect": "1.1.3", @@ -97,12 +92,13 @@ "@react-native-clipboard/clipboard": "1.16.3", "@react-native-community/blur": "^4.4.1", "@react-native-community/netinfo": "^11.4.1", + "@react-native-firebase/analytics": "^21.14.0", "@react-native-firebase/app": "^21.14.0", "@react-native-firebase/messaging": "^21.14.0", "@react-native-firebase/remote-config": "^21.14.0", "@react-native-google-signin/google-signin": "^16.1.2", - "@react-navigation/native": "^7.0.14", - "@react-navigation/native-stack": "^7.2.0", + "@react-navigation/native": "^7.2.2", + "@react-navigation/native-stack": "^7.14.10", "@robinbobin/react-native-google-drive-api-wrapper": "^2.2.6", "@segment/analytics-react-native": "^2.22.0", "@segment/sovran-react-native": "^1.1.3", @@ -112,19 +108,19 @@ "@sentry/react": "^9.32.0", "@sentry/react-native": "7.0.0", "@stablelib/cbor": "^2.0.4", - "@tamagui/animations-css": "1.126.14", - "@tamagui/animations-react-native": "1.126.14", - "@tamagui/config": "1.126.14", - "@tamagui/lucide-icons": "1.126.14", - "@tamagui/toast": "1.126.14", + "@tamagui/animations-css": "1.144.4", + "@tamagui/animations-react-native": "1.144.4", + "@tamagui/config": "1.144.4", + "@tamagui/lucide-icons": "1.144.4", + "@tamagui/toast": "1.144.4", "@turnkey/api-key-stamper": "^0.5.0", "@turnkey/core": "1.7.0", "@turnkey/encoding": "^0.6.0", "@turnkey/react-native-wallet-kit": "1.1.5", - "@walletconnect/react-native-compat": "^2.23.0", + "@walletconnect/react-native-compat": "^2.23.9", "@xstate/react": "^5.0.3", "asn1js": "^3.0.7", - "axios": "^1.14.0", + "axios": "^1.15.0", "buffer": "^6.0.3", "country-emoji": "^1.5.6", "elliptic": "^6.6.1", @@ -136,7 +132,7 @@ "js-sha256": "^0.11.1", "js-sha512": "^0.9.0", "lottie-react": "^2.4.1", - "lottie-react-native": "7.2.2", + "lottie-react-native": "7.3.6", "node-forge": "^1.4.0", "pkijs": "^3.4.0", "poseidon-lite": "^0.2.0", @@ -169,12 +165,12 @@ "react-native-svg-web": "1.0.9", "react-native-url-polyfill": "^3.0.0", "react-native-web": "^0.21.2", - "react-native-webview": "13.16.0", + "react-native-webview": "13.16.1", "react-qr-barcode-scanner": "^2.1.25", "socket.io-client": "^4.8.3", - "tamagui": "1.126.14", + "tamagui": "1.144.4", "uuid": "^11.1.0", - "xstate": "^5.20.2", + "xstate": "^5.30.0", "zustand": "^4.5.2" }, "devDependencies": { @@ -192,8 +188,8 @@ "@react-native/gradle-plugin": "0.77.0", "@react-native/metro-config": "0.77.0", "@react-native/typescript-config": "0.77.0", - "@tamagui/types": "1.126.14", - "@tamagui/vite-plugin": "1.126.14", + "@tamagui/types": "1.144.4", + "@tamagui/vite-plugin": "1.144.4", "@testing-library/react-native": "^13.3.3", "@tsconfig/react-native": "^3.0.9", "@types/dompurify": "^3.2.0", @@ -206,8 +202,8 @@ "@types/react-native-dotenv": "^0.2.2", "@types/react-native-sqlite-storage": "^6.0.5", "@types/react-native-web": "^0", - "@typescript-eslint/eslint-plugin": "^8.39.0", - "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/eslint-plugin": "^8.58.1", + "@typescript-eslint/parser": "^8.58.1", "@vitejs/plugin-react-swc": "^4.3.0", "babel-plugin-module-resolver": "^5.0.3", "babel-plugin-transform-remove-console": "^6.9.4", diff --git a/app/src/components/FeedbackModal.tsx b/app/src/components/FeedbackModal.tsx index 3be655be8..2a05a81fc 100644 --- a/app/src/components/FeedbackModal.tsx +++ b/app/src/components/FeedbackModal.tsx @@ -29,8 +29,8 @@ interface FeedbackModalProps { } const FeedbackModal: React.FC = ({ visible, onClose }) => { - const handleSupportForm = async () => { - await openSupportForm(); + const handleSupportForm = () => { + openSupportForm(); onClose(); }; diff --git a/app/src/components/PointHistoryList.tsx b/app/src/components/PointHistoryList.tsx deleted file mode 100644 index 0d18aaca7..000000000 --- a/app/src/components/PointHistoryList.tsx +++ /dev/null @@ -1,346 +0,0 @@ -// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. -// SPDX-License-Identifier: BUSL-1.1 -// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. - -import React, { useCallback, useMemo, useState } from 'react'; -import { - ActivityIndicator, - RefreshControl, - SectionList, - StyleSheet, -} from 'react-native'; -import { Card, Text, View, XStack, YStack } from 'tamagui'; - -import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; -import { PointEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; -import { - black, - blue600, - slate50, - slate200, - slate300, - slate400, - slate500, - white, -} from '@selfxyz/mobile-sdk-alpha/constants/colors'; -import { dinot, plexMono } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; - -import HeartIcon from '@/assets/icons/heart.svg'; -import StarBlackIcon from '@/assets/icons/star_black.svg'; -import type { PointEvent } from '@/services/points'; -import { usePointEventStore } from '@/stores/pointEventStore'; - -type Section = { - title: string; - data: PointEvent[]; -}; - -export type PointHistoryListProps = { - ListHeaderComponent?: - | React.ComponentType> - | React.ReactElement - | null; - onLayout?: () => void; -}; - -const TIME_PERIODS = { - TODAY: 'TODAY', - THIS_WEEK: 'THIS WEEK', - THIS_MONTH: 'THIS MONTH', - MONTH_NAME: (date: Date): string => { - return date.toLocaleString('default', { month: 'long' }).toUpperCase(); - }, - OLDER: 'OLDER', -}; - -const getIconForEventType = (type: PointEvent['type']) => { - switch (type) { - case 'disclosure': - return ; - default: - return ; - } -}; - -export const PointHistoryList: React.FC = ({ - ListHeaderComponent, - onLayout, -}) => { - const selfClient = useSelfClient(); - const [refreshing, setRefreshing] = useState(false); - const pointEvents = usePointEventStore(state => state.getAllPointEvents()); - const isLoading = usePointEventStore(state => state.isLoading); - const refreshPoints = usePointEventStore(state => state.refreshPoints); - const refreshIncomingPoints = usePointEventStore( - state => state.refreshIncomingPoints, - ); - const loadDisclosureEvents = usePointEventStore( - state => state.loadDisclosureEvents, - ); - - const formatDate = (timestamp: number) => { - return new Date(timestamp).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit', - }); - }; - - const formatDateFull = (timestamp: number) => { - return new Date(timestamp).toLocaleDateString([], { - month: 'short', - day: 'numeric', - }); - }; - - const getTimePeriod = useCallback((timestamp: number): string => { - const now = new Date(); - const eventDate = new Date(timestamp); - const startOfToday = new Date( - now.getFullYear(), - now.getMonth(), - now.getDate(), - ); - const startOfThisWeek = new Date(startOfToday); - startOfThisWeek.setDate(startOfToday.getDate() - startOfToday.getDay()); - const startOfThisMonth = new Date(now.getFullYear(), now.getMonth(), 1); - const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); - - if (eventDate >= startOfToday) { - return TIME_PERIODS.TODAY; - } else if (eventDate >= startOfThisWeek) { - return TIME_PERIODS.THIS_WEEK; - } else if (eventDate >= startOfThisMonth) { - return TIME_PERIODS.THIS_MONTH; - } else if (eventDate >= startOfLastMonth) { - return TIME_PERIODS.MONTH_NAME(eventDate); - } else { - return TIME_PERIODS.OLDER; - } - }, []); - - const groupedEvents = useMemo(() => { - const groups: Record = {}; - - [ - TIME_PERIODS.TODAY, - TIME_PERIODS.THIS_WEEK, - TIME_PERIODS.THIS_MONTH, - TIME_PERIODS.OLDER, - ].forEach(period => { - groups[period] = []; - }); - - const monthGroups = new Set(); - - pointEvents.forEach(event => { - const period = getTimePeriod(event.timestamp); - if ( - period !== TIME_PERIODS.TODAY && - period !== TIME_PERIODS.THIS_WEEK && - period !== TIME_PERIODS.THIS_MONTH && - period !== TIME_PERIODS.OLDER - ) { - monthGroups.add(period); - if (!groups[period]) { - groups[period] = []; - } - } - groups[period].push(event); - }); - - const sections: Section[] = []; - [ - TIME_PERIODS.TODAY, - TIME_PERIODS.THIS_WEEK, - TIME_PERIODS.THIS_MONTH, - ].forEach(period => { - if (groups[period] && groups[period].length > 0) { - sections.push({ title: period, data: groups[period] }); - } - }); - - Array.from(monthGroups) - .sort( - (a, b) => - new Date(groups[b][0].timestamp).getMonth() - - new Date(groups[a][0].timestamp).getMonth(), - ) - .forEach(month => { - sections.push({ title: month, data: groups[month] }); - }); - - if (groups[TIME_PERIODS.OLDER] && groups[TIME_PERIODS.OLDER].length > 0) { - sections.push({ - title: TIME_PERIODS.OLDER, - data: groups[TIME_PERIODS.OLDER], - }); - } - - return sections; - }, [pointEvents, getTimePeriod]); - - const renderItem = useCallback( - ({ - item, - index, - section, - }: { - item: PointEvent; - index: number; - section: Section; - }) => { - const borderRadiusSize = 16; - const isFirstItem = index === 0; - const isLastItem = index === section.data.length - 1; - - return ( - - - - - - {getIconForEventType(item.type)} - - - - {item.title} - - - {formatDateFull(item.timestamp)} •{' '} - {formatDate(item.timestamp)} - - - - +{item.points} - - - - - - ); - }, - [], - ); - - const renderSectionHeader = useCallback( - ({ section }: { section: Section }) => { - return ( - - - {section.title.toUpperCase()} - - - ); - }, - [], - ); - - const onRefresh = useCallback(() => { - selfClient.trackEvent(PointEvents.REFRESH_HISTORY); - setRefreshing(true); - Promise.all([ - refreshPoints(), - refreshIncomingPoints(), - loadDisclosureEvents(), - ]).finally(() => setRefreshing(false)); - }, [selfClient, refreshPoints, refreshIncomingPoints, loadDisclosureEvents]); - - const keyExtractor = useCallback((item: PointEvent) => item.id, []); - - const renderEmptyComponent = useCallback(() => { - if (isLoading) { - return ( - - - - Loading point history... - - - ); - } - return ( - - No point history available yet. - - Start earning points by completing actions! - - - ); - }, [isLoading]); - - return ( - - } - contentContainerStyle={[ - styles.listContent, - groupedEvents.length === 0 && styles.emptyList, - ]} - showsVerticalScrollIndicator={false} - stickySectionHeadersEnabled={false} - ListEmptyComponent={renderEmptyComponent} - ListHeaderComponent={ListHeaderComponent} - style={{ marginHorizontal: 15, marginBottom: 25 }} - onLayout={onLayout} - /> - ); -}; - -const styles = StyleSheet.create({ - listContent: { - paddingBottom: 100, - }, - emptyList: { - flexGrow: 1, - }, - emptyContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - paddingHorizontal: 20, - paddingTop: 5, - }, -}); - -export default PointHistoryList; diff --git a/app/src/components/navbar/DefaultNavBar.tsx b/app/src/components/navbar/DefaultNavBar.tsx index fdb7f89fe..b0fc0482c 100644 --- a/app/src/components/navbar/DefaultNavBar.tsx +++ b/app/src/components/navbar/DefaultNavBar.tsx @@ -47,9 +47,15 @@ export const DefaultNavBar = (props: NativeStackHeaderProps) => { {props.options.title} diff --git a/app/src/components/navbar/Points.tsx b/app/src/components/navbar/Points.tsx deleted file mode 100644 index 0085873c4..000000000 --- a/app/src/components/navbar/Points.tsx +++ /dev/null @@ -1,600 +0,0 @@ -// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. -// SPDX-License-Identifier: BUSL-1.1 -// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. - -import React, { useEffect, useState } from 'react'; -import { Pressable, StyleSheet } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { Button, Image, Text, View, XStack, YStack, ZStack } from 'tamagui'; -import { useFocusEffect, useNavigation } from '@react-navigation/native'; -import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import { HelpCircle } from '@tamagui/lucide-icons'; - -import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; -import { PointEvents } from '@selfxyz/mobile-sdk-alpha/constants/analytics'; -import { - black, - blue600, - slate50, - slate200, - slate500, - white, -} from '@selfxyz/mobile-sdk-alpha/constants/colors'; -import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; - -import BellWhiteIcon from '@/assets/icons/bell_white.svg'; -import ClockIcon from '@/assets/icons/clock.svg'; -import LockWhiteIcon from '@/assets/icons/lock_white.svg'; -import StarBlackIcon from '@/assets/icons/star_black.svg'; -import LogoInversed from '@/assets/images/logo_inversed.svg'; -import MajongImage from '@/assets/images/majong.png'; -import { PointHistoryList } from '@/components/PointHistoryList'; -import { appsUrl } from '@/consts/links'; -import { useIncomingPoints, usePoints } from '@/hooks/usePoints'; -import { usePointsGuardrail } from '@/hooks/usePointsGuardrail'; -import type { RootStackParamList } from '@/navigation'; -import { trackScreenView } from '@/services/analytics'; -import { - isTopicSubscribed, - requestNotificationPermission, - subscribeToTopics, -} from '@/services/notifications/notificationService'; -import { - formatTimeUntilDate, - POINT_VALUES, - recordBackupPointEvent, - recordNotificationPointEvent, -} from '@/services/points'; -import { usePointEventStore } from '@/stores/pointEventStore'; -import { useSettingStore } from '@/stores/settingStore'; -import { registerModalCallbacks } from '@/utils/modalCallbackRegistry'; - -const Points: React.FC = () => { - const selfClient = useSelfClient(); - - const { bottom } = useSafeAreaInsets(); - const navigation = - useNavigation>(); - const [isGeneralSubscribed, setIsGeneralSubscribed] = useState(false); - const [isEnabling, setIsEnabling] = useState(false); - const incomingPoints = useIncomingPoints(); - const { amount: points } = usePoints(); - const loadEvents = usePointEventStore(state => state.loadEvents); - const { hasCompletedBackupForPoints, setBackupForPointsCompleted } = - useSettingStore(); - const [isBackingUp, setIsBackingUp] = useState(false); - - // Guard: Validate that user has registered a document and completed points disclosure - usePointsGuardrail(); - - // Track NavBar view analytics - useFocusEffect( - React.useCallback(() => { - trackScreenView('Points NavBar', { - screenName: 'Points NavBar', - }); - }, []), - ); - - const onHelpButtonPress = () => { - navigation.navigate('PointsInfo'); - }; - - //TODO - uncomment after merging - https://github.com/selfxyz/self/pull/1363/ - // useEffect(() => { - // const backupEvent = usePointEventStore - // .getState() - // .events.find( - // event => event.type === 'backup' && event.status === 'completed', - // ); - - // if (backupEvent && !hasCompletedBackupForPoints) { - // setBackupForPointsCompleted(); - // } - // }, [setBackupForPointsCompleted, hasCompletedBackupForPoints]); - - // Track if we should check for backup completion on next focus - const shouldCheckBackupRef = React.useRef(false); - - // Detect when returning from backup screen and record points if backup was completed - useFocusEffect( - React.useCallback(() => { - const { cloudBackupEnabled, turnkeyBackupEnabled } = - useSettingStore.getState(); - const currentHasCompletedBackup = - useSettingStore.getState().hasCompletedBackupForPoints; - - // Only check if we explicitly set the flag (when navigating to backup settings) - // This prevents false triggers when returning from other flows (like notification permissions) - if ( - shouldCheckBackupRef.current && - (cloudBackupEnabled || turnkeyBackupEnabled) && - !currentHasCompletedBackup - ) { - const recordPoints = async () => { - try { - const response = await recordBackupPointEvent(); - - if (response.success) { - useSettingStore.getState().setBackupForPointsCompleted(); - selfClient.trackEvent(PointEvents.EARN_BACKUP_SUCCESS); - - const callbackId = registerModalCallbacks({ - onButtonPress: () => {}, - onModalDismiss: () => {}, - }); - navigation.navigate('Modal', { - titleText: 'Success!', - bodyText: `Account backed up successfully! You earned ${POINT_VALUES.backup} points.\n\nPoints will be distributed to your wallet on the next Sunday at noon UTC.`, - buttonText: 'OK', - callbackId, - }); - } else { - console.error( - 'Error recording backup points after return:', - response.error, - ); - selfClient.trackEvent(PointEvents.EARN_BACKUP_FAILED); - } - } catch (error) { - selfClient.trackEvent(PointEvents.EARN_BACKUP_FAILED); - console.error('Error recording backup points after return:', error); - } - }; - - recordPoints(); - } - - // Reset the flag after checking - shouldCheckBackupRef.current = false; - }, [navigation, selfClient]), - ); - - // Mock function to check if user has backed up their account - const hasUserBackedUpAccount = (): boolean => { - return hasCompletedBackupForPoints; - }; - - useEffect(() => { - loadEvents(); - }, [loadEvents]); - - useEffect(() => { - const checkSubscription = async () => { - const subscribed = await isTopicSubscribed('general'); - setIsGeneralSubscribed(subscribed); - }; - checkSubscription(); - }, []); - - const handleEnableNotifications = async () => { - if (isEnabling) { - return; - } - selfClient.trackEvent(PointEvents.EARN_NOTIFICATION); - setIsEnabling(true); - try { - const granted = await requestNotificationPermission(); - if (granted) { - const result = await subscribeToTopics(['general']); - if (result.successes.length > 0) { - const response = await recordNotificationPointEvent(); - if (response.success) { - setIsGeneralSubscribed(true); - selfClient.trackEvent(PointEvents.EARN_NOTIFICATION_SUCCESS); - - navigation.navigate('Gratification', { - points: POINT_VALUES.notification, - }); - } else { - selfClient.trackEvent(PointEvents.EARN_NOTIFICATION_FAILED, { - reason: 'Failed to record points', - }); - - const callbackId = registerModalCallbacks({ - onButtonPress: () => {}, - onModalDismiss: () => {}, - }); - navigation.navigate('Modal', { - titleText: 'Verification Failed', - bodyText: - response.error || - 'Failed to register points. Please try again.', - buttonText: 'OK', - callbackId, - }); - } - } else { - selfClient.trackEvent(PointEvents.EARN_NOTIFICATION_FAILED, { - reason: 'Subscription failed', - }); - const callbackId = registerModalCallbacks({ - onButtonPress: () => {}, - onModalDismiss: () => {}, - }); - navigation.navigate('Modal', { - titleText: 'Error', - bodyText: `Failed to enable: ${result.failures.map(f => f.error).join(', ')}`, - buttonText: 'OK', - callbackId, - }); - } - } else { - selfClient.trackEvent(PointEvents.EARN_NOTIFICATION_FAILED, { - reason: 'Permission denied', - }); - const callbackId = registerModalCallbacks({ - onButtonPress: () => {}, - onModalDismiss: () => {}, - }); - navigation.navigate('Modal', { - titleText: 'Permission Required', - bodyText: - 'Could not enable notifications. Please enable them in your device Settings.', - buttonText: 'OK', - callbackId, - }); - } - } catch (error) { - selfClient.trackEvent(PointEvents.EARN_NOTIFICATION_FAILED, { - reason: 'Exception occurred', - }); - const callbackId = registerModalCallbacks({ - onButtonPress: () => {}, - onModalDismiss: () => {}, - }); - navigation.navigate('Modal', { - titleText: 'Error', - bodyText: - error instanceof Error - ? error.message - : 'Failed to enable notifications', - buttonText: 'OK', - callbackId, - }); - } finally { - setIsEnabling(false); - } - }; - - const handleBackupSecret = async () => { - if (isBackingUp) { - return; - } - selfClient.trackEvent(PointEvents.EARN_BACKUP); - - const { cloudBackupEnabled, turnkeyBackupEnabled } = - useSettingStore.getState(); - - // If either backup method is already enabled, just record points - if (cloudBackupEnabled || turnkeyBackupEnabled) { - setIsBackingUp(true); - try { - // this will add event to store and the new event will then trigger useIncomingPoints hook to refetch incoming points - const response = await recordBackupPointEvent(); - - if (response.success) { - setBackupForPointsCompleted(); - selfClient.trackEvent(PointEvents.EARN_BACKUP_SUCCESS); - - navigation.navigate('Gratification', { - points: POINT_VALUES.backup, - }); - } else { - selfClient.trackEvent(PointEvents.EARN_BACKUP_FAILED); - const callbackId = registerModalCallbacks({ - onButtonPress: () => {}, - onModalDismiss: () => {}, - }); - navigation.navigate('Modal', { - titleText: 'Verification Failed', - bodyText: - response.error || 'Failed to register points. Please try again.', - buttonText: 'OK', - callbackId, - }); - } - } catch (error) { - selfClient.trackEvent(PointEvents.EARN_BACKUP_FAILED); - const callbackId = registerModalCallbacks({ - onButtonPress: () => {}, - onModalDismiss: () => {}, - }); - navigation.navigate('Modal', { - titleText: 'Error', - bodyText: - error instanceof Error ? error.message : 'Failed to backup account', - buttonText: 'OK', - callbackId, - }); - } finally { - setIsBackingUp(false); - } - } else { - // Navigate to backup screen and return to Points after backup completes - // Set flag to check for backup completion when we return - shouldCheckBackupRef.current = true; - navigation.navigate('CloudBackupSettings', { returnToScreen: 'Points' }); - } - }; - - const ListHeader = ( - - - - - - - - - - - - {`${points} Self points`} - - - Earn points by referring friends, disclosing proof requests, and - more. - - - - {incomingPoints && ( - - - - {`${incomingPoints.amount} incoming points`} - - - {`Expected in ${formatTimeUntilDate(incomingPoints.expectedDate)}`} - - - )} - - {!isGeneralSubscribed && ( - - - - - - - - {isEnabling - ? 'Enabling notifications...' - : 'Turn on push notifications'} - - - Earn {POINT_VALUES.notification} points - - - - - )} - {!hasUserBackedUpAccount() && ( - - - - - - - - {isBackingUp ? 'Processing backup...' : 'Backup your account'} - - - Earn {POINT_VALUES.backup} points - - - - - )} - { - selfClient.trackEvent(PointEvents.EARN_REFERRAL); - navigation.navigate('Referral'); - }} - > - - - - - - - - Refer friends and earn rewards - - Refer now - - - - - ); - - return ( - - - - - - - - - ); -}; - -const styles = StyleSheet.create({ - pointsCard: { - backgroundColor: white, - borderRadius: 10, - borderWidth: 1, - borderColor: slate200, - overflow: 'hidden', - }, - pointsCardContent: { - paddingVertical: 30, - paddingHorizontal: 40, - alignItems: 'center', - gap: 20, - }, - logoContainer: { - width: 68, - height: 68, - borderRadius: 12, - borderWidth: 1, - borderColor: slate200, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: white, - }, - pointsTitle: { - color: black, - textAlign: 'center', - fontFamily: dinot, - fontWeight: '500', - fontSize: 32, - lineHeight: 32, - letterSpacing: -1, - }, - pointsDescription: { - color: black, - fontFamily: dinot, - fontSize: 18, - fontWeight: '500', - textAlign: 'center', - paddingHorizontal: 20, - }, - incomingPointsBar: { - backgroundColor: slate50, - borderTopWidth: 1, - borderTopColor: slate200, - paddingVertical: 10, - paddingHorizontal: 10, - alignItems: 'center', - gap: 4, - }, - incomingPointsAmount: { - flex: 1, - fontFamily: dinot, - fontWeight: '500', - fontSize: 14, - color: black, - }, - incomingPointsTime: { - fontFamily: dinot, - fontWeight: '500', - fontSize: 14, - color: blue600, - }, - actionCard: { - gap: 22, - backgroundColor: white, - padding: 16, - borderRadius: 17, - borderWidth: 1, - borderColor: slate200, - }, - actionIconContainer: { - width: 60, - height: 60, - borderRadius: 16, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: black, - }, - actionTitle: { - color: black, - fontFamily: dinot, - fontWeight: '500', - fontSize: 16, - }, - actionSubtitle: { - color: slate500, - fontFamily: dinot, - fontSize: 14, - }, - referralCard: { - height: 270, - backgroundColor: white, - borderRadius: 16, - borderWidth: 1, - borderColor: slate200, - }, - referralImageContainer: { - borderBottomWidth: 1, - borderBottomColor: slate200, - height: 170, - }, - referralImage: { - width: '80%', - height: '100%', - position: 'absolute', - right: 0, - top: 0, - }, - referralStarIcon: { - marginLeft: 16, - marginTop: 16, - }, - referralTitle: { - fontFamily: dinot, - fontSize: 16, - color: black, - }, - referralLink: { - fontFamily: dinot, - fontSize: 16, - color: blue600, - }, - blurView: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - height: 100, - }, - exploreButtonContainer: { - position: 'absolute', - left: 20, - right: 20, - }, - exploreButton: { - backgroundColor: black, - paddingHorizontal: 20, - paddingVertical: 14, - borderRadius: 5, - height: 52, - }, - exploreButtonText: { - fontFamily: dinot, - fontSize: 16, - color: white, - textAlign: 'center', - }, - helpButton: { - position: 'absolute', - top: 0, - right: 0, - padding: 12, - }, -}); - -export default Points; diff --git a/app/src/components/navbar/PointsNavBar.tsx b/app/src/components/navbar/PointsNavBar.tsx deleted file mode 100644 index 1e2f3a621..000000000 --- a/app/src/components/navbar/PointsNavBar.tsx +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. -// SPDX-License-Identifier: BUSL-1.1 -// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. - -import React from 'react'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import type { NativeStackHeaderProps } from '@react-navigation/native-stack'; - -import { Text, View } from '@selfxyz/mobile-sdk-alpha/components'; -import { black, slate50 } from '@selfxyz/mobile-sdk-alpha/constants/colors'; - -import { NavBar } from '@/components/navbar/BaseNavBar'; -import { buttonTap } from '@/integrations/haptics'; -import { extraYPadding } from '@/utils/styleUtils'; - -export const PointsNavBar = (props: NativeStackHeaderProps) => { - const insets = useSafeAreaInsets(); - const closeButtonWidth = 50; - - return ( - - { - buttonTap(); - props.navigation.navigate('Home'); - }} - /> - - - Self Points - - - - } - /> - - ); -}; diff --git a/app/src/consts/links.ts b/app/src/consts/links.ts index a2747fabd..921a6fb52 100644 --- a/app/src/consts/links.ts +++ b/app/src/consts/links.ts @@ -14,7 +14,6 @@ export const apiBaseUrl = 'https://api.self.xyz'; export const apiPingUrl = 'https://api.self.xyz/ping'; export const appStoreUrl = 'https://apps.apple.com/app/self-zk/id6478563710'; export const appleICloudDocsUrl = 'https://support.apple.com/en-us/102651'; -export const appsUrl = 'https://apps.self.xyz'; export const discordUrl = 'https://discord.gg/selfxyz'; export const gitHubUrl = 'https://github.com/selfxyz/self'; export const googleDriveAppDataScope = diff --git a/app/src/hooks/useEarnPointsFlow.ts b/app/src/hooks/useEarnPointsFlow.ts deleted file mode 100644 index a7881780b..000000000 --- a/app/src/hooks/useEarnPointsFlow.ts +++ /dev/null @@ -1,203 +0,0 @@ -// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. -// SPDX-License-Identifier: BUSL-1.1 -// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. - -import { useCallback } from 'react'; -import { useNavigation } from '@react-navigation/native'; -import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; - -import { useSelfClient } from '@selfxyz/mobile-sdk-alpha'; - -import { useRegisterReferral } from '@/hooks/useRegisterReferral'; -import type { RootStackParamList } from '@/navigation'; -import { - hasUserAnIdentityDocumentRegistered, - hasUserDoneThePointsDisclosure, - POINT_VALUES, - pointsSelfApp, -} from '@/services/points'; -import useUserStore from '@/stores/userStore'; -import { registerModalCallbacks } from '@/utils/modalCallbackRegistry'; - -type UseEarnPointsFlowParams = { - hasReferrer: boolean; - isReferralConfirmed: boolean | undefined; -}; - -export const useEarnPointsFlow = ({ - hasReferrer, - isReferralConfirmed, -}: UseEarnPointsFlowParams) => { - const selfClient = useSelfClient(); - const navigation = - useNavigation>(); - const { registerReferral } = useRegisterReferral(); - const referrer = useUserStore(state => state.deepLinkReferrer); - - const navigateToPointsProof = useCallback(async () => { - const selfApp = await pointsSelfApp(); - selfClient.getSelfAppState().setSelfApp(selfApp); - - // Use setTimeout to ensure modal dismisses before navigating - setTimeout(() => { - navigation.navigate('ProvingScreenRouter'); - }, 100); - }, [selfClient, navigation]); - - const showIdentityVerificationModal = useCallback(() => { - const callbackId = registerModalCallbacks({ - onButtonPress: () => { - // Use setTimeout to ensure modal dismisses before navigating - setTimeout(() => { - navigation.navigate('CountryPicker'); - }, 100); - }, - onModalDismiss: () => { - if (hasReferrer) { - useUserStore.getState().clearDeepLinkReferrer(); - } - }, - }); - - navigation.navigate('Modal', { - titleText: 'Identity Verification Required', - bodyText: - 'To access Self Points, you need to register an identity document with Self first. This helps us verify your identity and keep your points secure.', - buttonText: 'Verify Identity', - secondaryButtonText: 'Not Now', - callbackId, - }); - }, [hasReferrer, navigation]); - - const showPointsDisclosureModal = useCallback(() => { - const callbackId = registerModalCallbacks({ - onButtonPress: () => { - navigateToPointsProof(); - }, - onModalDismiss: () => { - if (hasReferrer) { - useUserStore.getState().clearDeepLinkReferrer(); - } - }, - }); - - navigation.navigate('Modal', { - titleText: 'Points Disclosure Required', - bodyText: - 'To access Self Points, you need to complete the points disclosure first. This helps us verify your identity and keep your points secure.', - buttonText: 'Complete Points Disclosure', - secondaryButtonText: 'Not Now', - callbackId, - }); - }, [hasReferrer, navigation, navigateToPointsProof]); - - const showPointsInfoScreen = useCallback(() => { - const callbackId = registerModalCallbacks({ - onButtonPress: () => { - showPointsDisclosureModal(); - }, - onModalDismiss: () => { - if (hasReferrer) { - useUserStore.getState().clearDeepLinkReferrer(); - } - }, - }); - - navigation.navigate('PointsInfo', { - showNextButton: true, - callbackId, - }); - }, [hasReferrer, navigation, showPointsDisclosureModal]); - - const handleReferralFlow = useCallback(async () => { - if (!referrer) { - return; - } - - const showReferralErrorModal = (errorMessage: string) => { - const callbackId = registerModalCallbacks({ - onButtonPress: async () => { - await handleReferralFlow(); - }, - onModalDismiss: () => { - // Clear referrer when user dismisses to prevent retry loop - useUserStore.getState().clearDeepLinkReferrer(); - }, - }); - - navigation.navigate('Modal', { - titleText: 'Referral Registration Failed', - bodyText: `We couldn't register your referral at this time. ${errorMessage}. You can try again or dismiss this message.`, - buttonText: 'Try Again', - secondaryButtonText: 'Dismiss', - callbackId, - }); - }; - - const store = useUserStore.getState(); - // Check if already registered to avoid duplicate calls - if (!store.isReferrerRegistered(referrer)) { - const result = await registerReferral(referrer); - if (result.success) { - store.markReferrerAsRegistered(referrer); - - // Only navigate to GratificationScreen on success - store.clearDeepLinkReferrer(); - navigation.navigate('Gratification', { - points: POINT_VALUES.referee, - }); - } else { - // Registration failed - show error and preserve referrer - const errorMessage = result.error || 'Unknown error occurred'; - console.error('Referral registration failed:', errorMessage); - - // Show error modal with retry option, don't clear referrer - showReferralErrorModal(errorMessage); - } - } else { - // Already registered, navigate to gratification - store.clearDeepLinkReferrer(); - navigation.navigate('Gratification', { - points: POINT_VALUES.referee, - }); - } - }, [referrer, registerReferral, navigation]); - - const onEarnPointsPress = useCallback( - async (skipReferralFlow = true) => { - const hasUserAnIdentityDocumentRegistered_result = - await hasUserAnIdentityDocumentRegistered(); - if (!hasUserAnIdentityDocumentRegistered_result) { - showIdentityVerificationModal(); - return; - } - - const hasUserDoneThePointsDisclosure_result = - await hasUserDoneThePointsDisclosure(); - if (!hasUserDoneThePointsDisclosure_result) { - showPointsInfoScreen(); - return; - } - - // User has completed both checks - if (!skipReferralFlow && hasReferrer && isReferralConfirmed === true) { - await handleReferralFlow(); - } else { - // Just go to points upon pressing "Earn Points" button - if (!hasReferrer) { - navigation.navigate('Points'); - } - } - }, - [ - hasReferrer, - isReferralConfirmed, - navigation, - showIdentityVerificationModal, - showPointsInfoScreen, - handleReferralFlow, - ], - ); - - return { onEarnPointsPress }; -}; diff --git a/app/src/hooks/useDiditLauncher.ts b/app/src/hooks/useKycLauncher.ts similarity index 78% rename from app/src/hooks/useDiditLauncher.ts rename to app/src/hooks/useKycLauncher.ts index 8a95f89ed..0c9dbe07b 100644 --- a/app/src/hooks/useDiditLauncher.ts +++ b/app/src/hooks/useKycLauncher.ts @@ -8,28 +8,31 @@ import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { sanitizeErrorMessage } from '@selfxyz/mobile-sdk-alpha'; -import { createSession, launchDidit } from '@/integrations/didit'; -import type { DiditVerificationResult } from '@/integrations/didit/types'; +import { + createKycSession, + launchKycVerification as startKycVerification, +} from '@/integrations/kyc'; +import type { KycVerificationResult } from '@/integrations/kyc/types'; import type { RootStackParamList } from '@/navigation'; export type FallbackErrorSource = 'mrz_scan_failed' | 'nfc_scan_failed'; -export interface UseDiditLauncherOptions { +export interface UseKycLauncherOptions { /** * Country code for the user's document */ countryCode: string; /** - * Error source to track where the Didit launch was initiated from + * Error source to track where the KYC launch was initiated from */ errorSource: FallbackErrorSource; /** * Optional callback to handle successful verification. - * Receives the Didit result and the sessionId from the session. + * Receives the KYC result and the sessionId from the session. * If not provided, defaults to navigating to KycSuccess with the sessionId. */ onSuccess?: ( - result: DiditVerificationResult, + result: KycVerificationResult, sessionId: string, ) => void | Promise; /** @@ -41,42 +44,42 @@ export interface UseDiditLauncherOptions { */ onError?: ( error: unknown, - result?: DiditVerificationResult, + result?: KycVerificationResult, ) => void | Promise; } /** - * Custom hook for launching Didit verification with consistent error handling. + * Custom hook for launching KYC verification with consistent error handling. * * Abstracts the common pattern of: * 1. Creating a session - * 2. Launching Didit SDK + * 2. Launching the provider SDK * 3. Handling errors by navigating to fallback screen * 4. Managing loading state * * @example * ```tsx - * const { launchDiditVerification, isLoading } = useDiditLauncher({ + * const { launchKycVerification, isLoading } = useKycLauncher({ * countryCode: 'US', * errorSource: 'nfc_scan_failed', * }); * - * * ``` */ -export const useDiditLauncher = (options: UseDiditLauncherOptions) => { +export const useKycLauncher = (options: UseKycLauncherOptions) => { const { countryCode, errorSource, onSuccess, onCancel, onError } = options; const navigation = useNavigation>(); const [isLoading, setIsLoading] = useState(false); - const launchDiditVerification = useCallback(async () => { + const launchKycVerification = useCallback(async () => { setIsLoading(true); try { - const session = await createSession(); - const result = await launchDidit(session.sessionToken); + const session = await createKycSession(); + const result = await startKycVerification(session.sessionToken); // Handle user cancellation if (result.type === 'cancelled') { @@ -89,7 +92,7 @@ export const useDiditLauncher = (options: UseDiditLauncherOptions) => { const error = result.error?.message || result.error?.type || 'Unknown error'; const safeError = sanitizeErrorMessage(error); - console.error('Didit verification failed:', safeError); + console.error('KYC verification failed:', safeError); // Call custom error handler if provided, otherwise navigate to fallback screen if (onError) { @@ -134,7 +137,7 @@ export const useDiditLauncher = (options: UseDiditLauncherOptions) => { }, [navigation, countryCode, errorSource, onSuccess, onCancel, onError]); return { - launchDiditVerification, + launchKycVerification, isLoading, }; }; diff --git a/app/src/hooks/useDiditWebSocket.ts b/app/src/hooks/useKycWebSocket.ts similarity index 78% rename from app/src/hooks/useDiditWebSocket.ts rename to app/src/hooks/useKycWebSocket.ts index 323b55439..808a0026d 100644 --- a/app/src/hooks/useDiditWebSocket.ts +++ b/app/src/hooks/useKycWebSocket.ts @@ -4,17 +4,20 @@ import { useCallback, useRef } from 'react'; import { io, type Socket } from 'socket.io-client'; -import { DIDIT_TEE_URL } from '@env'; +import { KYC_TEE_URL } from '@env'; import { deserializeApplicantInfo } from '@selfxyz/common'; import type { DocumentType, KycData } from '@selfxyz/common/utils/types'; -import type { ApplicantInfoSerialized } from '@/integrations/didit/types'; +import type { ApplicantInfoSerialized } from '@/integrations/kyc/types'; import { navigationRef } from '@/navigation'; import { storeDocumentWithDeduplication } from '@/providers/passportDataProvider'; import { usePendingKycStore } from '@/stores/pendingKycStore'; -interface UseDiditWebSocketOptions { +const redactSessionId = (id: string) => + id.length > 8 ? `${id.slice(0, 4)}***${id.slice(-4)}` : '***'; + +interface UseKycWebSocketOptions { onSuccess?: () => void; onError?: (error: string) => void; onVerificationFailed?: (reason: string) => void; @@ -22,11 +25,11 @@ interface UseDiditWebSocketOptions { } /** - * Shared hook for Didit websocket subscription logic. + * Shared hook for KYC websocket subscription logic. * Handles connecting to the TEE service, subscribing to a sessionId, * and processing verification results. */ -export function useDiditWebSocket(options: UseDiditWebSocketOptions = {}) { +export function useKycWebSocket(options: UseKycWebSocketOptions = {}) { const { onSuccess, onError, @@ -51,8 +54,8 @@ export function useDiditWebSocket(options: UseDiditWebSocketOptions = {}) { (sessionId: string) => { if (subscribedSessionIdsRef.current.has(sessionId)) { console.log( - '[DiditWebSocket] Already subscribed to sessionId:', - sessionId, + '[KycWebSocket] Already subscribed to sessionId:', + redactSessionId(sessionId), ); return; } @@ -63,23 +66,23 @@ export function useDiditWebSocket(options: UseDiditWebSocketOptions = {}) { // Don't retry 'processing' verifications as the proving machine is reading to be triggered. if (isProcessing) { console.log( - '[DiditWebSocket] Verification in processing state, skipping for sessionId:', - sessionId, + '[KycWebSocket] Verification in processing state, skipping for sessionId:', + redactSessionId(sessionId), ); return; } if (!skipAddPending) { console.log( - '[DiditWebSocket] Adding pending verification for sessionId:', - sessionId, + '[KycWebSocket] Adding pending verification for sessionId:', + redactSessionId(sessionId), ); addPendingVerification(sessionId); } subscribedSessionIdsRef.current.add(sessionId); - console.log('[DiditWebSocket] Connecting to WebSocket:', DIDIT_TEE_URL); - const socket = io(DIDIT_TEE_URL, { + console.log('[KycWebSocket] Connecting to WebSocket:', KYC_TEE_URL); + const socket = io(KYC_TEE_URL, { transports: ['websocket', 'polling'], }); @@ -87,16 +90,16 @@ export function useDiditWebSocket(options: UseDiditWebSocketOptions = {}) { socket.on('connect', () => { console.log( - '[DiditWebSocket] Connected, subscribing to user:', - sessionId, + '[KycWebSocket] Connected, subscribing to user:', + redactSessionId(sessionId), ); socket.emit('subscribe', sessionId); }); socket.on('success', async (data: ApplicantInfoSerialized) => { console.log( - '[DiditWebSocket] Received applicant info for sessionId:', - sessionId, + '[KycWebSocket] Received applicant info for sessionId:', + redactSessionId(sessionId), ); try { @@ -113,7 +116,7 @@ export function useDiditWebSocket(options: UseDiditWebSocketOptions = {}) { }; const documentId = await storeDocumentWithDeduplication(kycData); console.log( - '[DiditWebSocket] KYC data stored successfully, documentId:', + '[KycWebSocket] KYC data stored successfully, documentId:', documentId, ); @@ -131,7 +134,7 @@ export function useDiditWebSocket(options: UseDiditWebSocketOptions = {}) { socket.emit('ack_success', sessionId); onSuccess?.(); } catch (err) { - console.error('[DiditWebSocket] Failed to store KYC data:', err); + console.error('[KycWebSocket] Failed to store KYC data:', err); updateVerificationStatus( sessionId, 'failed', @@ -146,7 +149,7 @@ export function useDiditWebSocket(options: UseDiditWebSocketOptions = {}) { }); socket.on('verification_failed', (reason: string) => { - console.log('[DiditWebSocket] Verification failed:', reason); + console.log('[KycWebSocket] Verification failed:', reason); updateVerificationStatus(sessionId, 'failed', reason); onVerificationFailed?.(reason); @@ -156,7 +159,7 @@ export function useDiditWebSocket(options: UseDiditWebSocketOptions = {}) { }); socket.on('error', (errorMessage: string) => { - console.error('[DiditWebSocket] Socket error:', errorMessage); + console.error('[KycWebSocket] Socket error:', errorMessage); updateVerificationStatus(sessionId, 'failed', errorMessage); onError?.(errorMessage); @@ -166,7 +169,10 @@ export function useDiditWebSocket(options: UseDiditWebSocketOptions = {}) { }); socket.on('disconnect', () => { - console.log('[DiditWebSocket] Disconnected for sessionId:', sessionId); + console.log( + '[KycWebSocket] Disconnected for sessionId:', + redactSessionId(sessionId), + ); }); }, [ diff --git a/app/src/hooks/useOpenSupportForm.ts b/app/src/hooks/useOpenSupportForm.ts new file mode 100644 index 000000000..ed295a4cc --- /dev/null +++ b/app/src/hooks/useOpenSupportForm.ts @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import { useCallback } from 'react'; + +import { impactLight } from '@/integrations/haptics'; +import { openSupportForm } from '@/services/support'; + +/** + * Hook wrapper around openSupportForm that adds haptic feedback. + * Use this inside screen components. For code outside the navigation tree + * (providers, modals rendered at root), call openSupportForm() directly. + */ +const useOpenSupportForm = () => + useCallback(() => { + impactLight(); + openSupportForm(); + }, []); + +export default useOpenSupportForm; diff --git a/app/src/hooks/usePendingKycRecovery.ts b/app/src/hooks/usePendingKycRecovery.ts index f308e2dc1..13a5b7222 100644 --- a/app/src/hooks/usePendingKycRecovery.ts +++ b/app/src/hooks/usePendingKycRecovery.ts @@ -4,7 +4,7 @@ import { useCallback, useEffect, useRef } from 'react'; -import { useDiditWebSocket } from '@/hooks/useDiditWebSocket'; +import { useKycWebSocket } from '@/hooks/useKycWebSocket'; import { navigationRef } from '@/navigation'; import { usePendingKycStore } from '@/stores/pendingKycStore'; @@ -28,7 +28,7 @@ function getRecoveryIdentifier(verification: RecoveryVerification) { * 2. For each non-expired pending/processing verification, reconnects to websocket * 3. Subscribes to the sessionId to receive any cached results * 4. Updates verification status based on server response - * 5. Initiates proving machine after document storage (handled in useDiditWebSocket) + * 5. Initiates proving machine after document storage (handled in useKycWebSocket) * * NOTE: This requires the TEE server to cache completed verification results * so they can be retrieved when the app reopens. @@ -51,7 +51,7 @@ export function usePendingKycRecovery() { console.log('[PendingKycRecovery] Verification failed:', reason); }, []); - const { subscribe, unsubscribeAll } = useDiditWebSocket({ + const { subscribe, unsubscribeAll } = useKycWebSocket({ skipAddPending: true, onSuccess: handleSuccess, onError: handleError, diff --git a/app/src/hooks/usePoints.ts b/app/src/hooks/usePoints.ts deleted file mode 100644 index 8330bd361..000000000 --- a/app/src/hooks/usePoints.ts +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. -// SPDX-License-Identifier: BUSL-1.1 -// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. - -import { useEffect } from 'react'; - -import { getNextSundayNoonUTC, type IncomingPoints } from '@/services/points'; -import { usePointEventStore } from '@/stores/pointEventStore'; - -/* - * Hook to get incoming points for the user. It shows the optimistic incoming points. - * Refreshes incoming points once on mount. - */ -export const useIncomingPoints = (): IncomingPoints => { - const incomingPoints = usePointEventStore(state => state.incomingPoints); - const totalOptimisticIncomingPoints = usePointEventStore(state => - state.totalOptimisticIncomingPoints(), - ); - const refreshIncomingPoints = usePointEventStore( - state => state.refreshIncomingPoints, - ); - - useEffect(() => { - // Only refresh once on mount - the store handles promise caching for concurrent calls - refreshIncomingPoints(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); // Empty deps: only run once on mount - - return { - amount: totalOptimisticIncomingPoints, - expectedDate: incomingPoints.expectedDate, - }; -}; - -/* - * Hook to fetch total points for the user. It refetches the total points when the next points update time is reached (each Sunday noon UTC). - */ -export const usePoints = () => { - const points = usePointEventStore(state => state.points); - const nextPointsUpdate = getNextSundayNoonUTC().getTime(); - const refreshPoints = usePointEventStore(state => state.refreshPoints); - - useEffect(() => { - refreshPoints(); - // refresh when points update time changes as its the only time points can change - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [nextPointsUpdate]); - - return { - amount: points, - refetch: refreshPoints, - }; -}; diff --git a/app/src/hooks/usePointsGuardrail.ts b/app/src/hooks/usePointsGuardrail.ts deleted file mode 100644 index e22bf8f7b..000000000 --- a/app/src/hooks/usePointsGuardrail.ts +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. -// SPDX-License-Identifier: BUSL-1.1 -// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. - -import { useCallback } from 'react'; -import { useFocusEffect, useNavigation } from '@react-navigation/native'; -import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; - -import type { RootStackParamList } from '@/navigation'; -import { - hasUserAnIdentityDocumentRegistered, - hasUserDoneThePointsDisclosure, -} from '@/services/points'; - -/** - * Guard hook that validates points screen access requirements. - * Redirects to Home if user hasn't: - * 1. Registered an identity document - * 2. Completed the points disclosure - * - * This prevents users from accessing the Points screen through: - * - GratificationScreen's "Explore rewards" button - * - CloudBackupSettings return paths - * - Any other navigation bypass - */ -export const usePointsGuardrail = () => { - const navigation = - useNavigation>(); - - useFocusEffect( - useCallback(() => { - let isActive = true; - - const checkRequirements = async () => { - const hasDocument = await hasUserAnIdentityDocumentRegistered(); - const hasDisclosed = await hasUserDoneThePointsDisclosure(); - - // Only navigate if the screen is still focused - if (isActive && (!hasDocument || !hasDisclosed)) { - // User hasn't met requirements, redirect to Home - navigation.navigate('Home', {}); - } - }; - checkRequirements(); - - return () => { - isActive = false; - }; - }, [navigation]), - ); -}; diff --git a/app/src/hooks/useTestReferralFlow.ts b/app/src/hooks/useTestReferralFlow.ts index 7438f5cc7..e655fe517 100644 --- a/app/src/hooks/useTestReferralFlow.ts +++ b/app/src/hooks/useTestReferralFlow.ts @@ -13,8 +13,7 @@ const TEST_REFERRER = '0x1234567890123456789012345678901234567890'; * Hook for testing referral flow in DEV mode. * Provides automatic timeout trigger (3 seconds) and manual trigger function. * - * Flow: Sets referrer → shows confirmation modal → on confirm, checks prerequisites - * → if identity doc & points disclosure done → registers referral → navigates to Gratification + * Flow: Sets referrer → shows confirmation modal → on confirm → registers referral * * @param shouldAutoTrigger - Whether to automatically trigger the flow after 3 seconds (default: false) */ diff --git a/app/src/integrations/didit/index.ts b/app/src/integrations/kyc/index.ts similarity index 57% rename from app/src/integrations/didit/index.ts rename to app/src/integrations/kyc/index.ts index b9b2684a7..139398a0a 100644 --- a/app/src/integrations/didit/index.ts +++ b/app/src/integrations/kyc/index.ts @@ -4,11 +4,11 @@ export type { ApplicantInfoSerialized, - DiditVerificationResult, + KycVerificationResult, SessionResponse, -} from '@/integrations/didit/types'; +} from '@/integrations/kyc/types'; export { - type DiditConfig, - createSession, - launchDidit, -} from '@/integrations/didit/diditService'; + type KycLaunchConfig, + createKycSession, + launchKycVerification, +} from '@/integrations/kyc/kycService'; diff --git a/app/src/integrations/didit/diditService.ts b/app/src/integrations/kyc/kycService.ts similarity index 65% rename from app/src/integrations/didit/diditService.ts rename to app/src/integrations/kyc/kycService.ts index 2c0f19988..954452a00 100644 --- a/app/src/integrations/didit/diditService.ts +++ b/app/src/integrations/kyc/kycService.ts @@ -3,22 +3,22 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. import { startVerification } from '@didit-protocol/sdk-react-native'; -import { DIDIT_TEE_URL } from '@env'; +import { KYC_TEE_URL } from '@env'; import type { - DiditVerificationResult, + KycVerificationResult, SessionResponse, -} from '@/integrations/didit/types'; +} from '@/integrations/kyc/types'; -export interface DiditConfig { +export interface KycLaunchConfig { locale?: string; debug?: boolean; } const FETCH_TIMEOUT_MS = 30000; -export const createSession = async (): Promise => { - const apiUrl = DIDIT_TEE_URL; +export const createKycSession = async (): Promise => { + const apiUrl = KYC_TEE_URL; console.log('[Didit] createSession URL:', apiUrl); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); @@ -36,9 +36,7 @@ export const createSession = async (): Promise => { clearTimeout(timeoutId); if (!response.ok) { - throw new Error( - `Failed to create Didit session (HTTP ${response.status})`, - ); + throw new Error(`Failed to create KYC session (HTTP ${response.status})`); } const body = await response.json(); @@ -54,24 +52,24 @@ export const createSession = async (): Promise => { if (err instanceof Error) { if (err.name === 'AbortError') { throw new Error( - `Request to Didit TEE timed out after ${FETCH_TIMEOUT_MS / 1000}s`, + `Request to KYC TEE timed out after ${FETCH_TIMEOUT_MS / 1000}s`, ); } - throw new Error(`Failed to create Didit session: ${err.message}`); + throw new Error(`Failed to create KYC session: ${err.message}`); } - throw new Error('Failed to create Didit session: Unknown error'); + throw new Error('Failed to create KYC session: Unknown error'); } }; -export const launchDidit = async ( +export const launchKycVerification = async ( sessionToken: string, - config?: DiditConfig, -): Promise => { + config?: KycLaunchConfig, +): Promise => { const result = await startVerification(sessionToken, { languageCode: config?.locale ?? 'en', loggingEnabled: config?.debug ?? __DEV__, }); - return result as DiditVerificationResult; + return result as KycVerificationResult; }; diff --git a/app/src/integrations/didit/types.ts b/app/src/integrations/kyc/types.ts similarity index 94% rename from app/src/integrations/didit/types.ts rename to app/src/integrations/kyc/types.ts index 8f0e51ccd..311066b5f 100644 --- a/app/src/integrations/didit/types.ts +++ b/app/src/integrations/kyc/types.ts @@ -8,7 +8,7 @@ export interface ApplicantInfoSerialized { pubkey: Array; } -export interface DiditVerificationResult { +export interface KycVerificationResult { type: 'completed' | 'cancelled' | 'failed'; session?: { status: string; diff --git a/app/src/navigation/account.ts b/app/src/navigation/account.ts index 6f4c9dc1d..151e168ef 100644 --- a/app/src/navigation/account.ts +++ b/app/src/navigation/account.ts @@ -58,6 +58,7 @@ const accountScreens = { screen: CloudBackupScreen, options: { title: 'Account Backup', + headerTintColor: black, headerStyle: { backgroundColor: white, }, @@ -70,6 +71,7 @@ const accountScreens = { screen: ProofSettingsScreen, options: { title: 'Proof Settings', + headerTintColor: black, headerStyle: { backgroundColor: white, }, @@ -83,6 +85,8 @@ const accountScreens = { options: { animation: 'slide_from_bottom', title: 'Settings', + headerBackTitle: 'close', + headerTintColor: black, headerStyle: { backgroundColor: white, }, @@ -106,6 +110,7 @@ const accountScreens = { } as NativeStackNavigationOptions) : ({ title: 'Recovery Phrase', + headerTintColor: black, headerStyle: { backgroundColor: white, }, diff --git a/app/src/navigation/app.tsx b/app/src/navigation/app.tsx index 766bed085..0b3b2d889 100644 --- a/app/src/navigation/app.tsx +++ b/app/src/navigation/app.tsx @@ -9,7 +9,6 @@ import type { DocumentCategory } from '@selfxyz/common/utils/types'; import { SystemBars } from '@/components/SystemBars'; import DeferredLinkingInfoScreen from '@/screens/app/DeferredLinkingInfoScreen'; -import GratificationScreen from '@/screens/app/GratificationScreen'; import LoadingScreen from '@/screens/app/LoadingScreen'; import type { ModalNavigationParams } from '@/screens/app/ModalScreen'; import ModalScreen from '@/screens/app/ModalScreen'; @@ -50,16 +49,6 @@ const appScreens = { header: () => , }, }, - Gratification: { - screen: GratificationScreen, - options: { - headerShown: false, - contentStyle: { backgroundColor: '#000000' }, - } as NativeStackNavigationOptions, - params: {} as { - points?: number; - }, - }, }; export default appScreens; diff --git a/app/src/navigation/deeplinks.ts b/app/src/navigation/deeplinks.ts index 108d12d03..67daa7b48 100644 --- a/app/src/navigation/deeplinks.ts +++ b/app/src/navigation/deeplinks.ts @@ -102,35 +102,12 @@ const createDeeplinkNavigationState = ( // Store the correct parent screen determined by splash screen let correctParentScreen: string = 'Home'; -// Function for splash screen to get and clear the queued initial URL export const getAndClearQueuedUrl = (): string | null => { const url = queuedInitialUrl; queuedInitialUrl = null; return url; }; -const safeNavigate = ( - navigationState: ReturnType, -): void => { - const targetScreen = navigationState.routes[1]?.name as - | keyof RootStackParamList - | undefined; - - const currentRoute = navigationRef.getCurrentRoute(); - const isColdLaunch = currentRoute?.name === 'Splash'; - - if (!isColdLaunch && targetScreen) { - // Use object syntax to satisfy TypeScript's strict typing for navigate - // The params will be undefined for screens that don't require them - navigationRef.navigate({ - name: targetScreen, - params: undefined, - } as Parameters[0]); - } else { - navigationRef.reset(navigationState); - } -}; - export const handleUrl = (selfClient: SelfClient, uri: string) => { const validatedParams = parseAndValidateUrlParams(uri); const { @@ -241,6 +218,28 @@ export const handleUrl = (selfClient: SelfClient, uri: string) => { } }; +const safeNavigate = ( + navigationState: ReturnType, +): void => { + const targetScreen = navigationState.routes[1]?.name as + | keyof RootStackParamList + | undefined; + + const currentRoute = navigationRef.getCurrentRoute(); + const isColdLaunch = currentRoute?.name === 'Splash'; + + if (!isColdLaunch && targetScreen) { + // Use object syntax to satisfy TypeScript's strict typing for navigate + // The params will be undefined for screens that don't require them + navigationRef.navigate({ + name: targetScreen, + params: undefined, + } as Parameters[0]); + } else { + navigationRef.reset(navigationState); + } +}; + /** * Parses and validates query parameters from a URL * @param uri - The URL to parse @@ -285,6 +284,9 @@ export const parseAndValidateUrlParams = (uri: string): ValidatedParams => { return validatedParams; }; +// Function for splash screen to get and clear the queued initial URL +export const peekQueuedUrl = (): string | null => queuedInitialUrl; + // Store the initial URL for splash screen to handle after initialization let queuedInitialUrl: string | null = null; diff --git a/app/src/navigation/documents.ts b/app/src/navigation/documents.ts index e28d353f4..142208400 100644 --- a/app/src/navigation/documents.ts +++ b/app/src/navigation/documents.ts @@ -126,6 +126,7 @@ const documentsScreens = { screen: ManageDocumentsScreen, options: { title: 'Manage Documents', + headerTintColor: black, headerStyle: { backgroundColor: white, }, @@ -138,6 +139,7 @@ const documentsScreens = { screen: DocumentDataInfoScreen, options: { title: 'Document Data Info', + headerTintColor: black, headerStyle: { backgroundColor: white, }, diff --git a/app/src/navigation/home.ts b/app/src/navigation/home.ts index 73a22babb..3675ae455 100644 --- a/app/src/navigation/home.ts +++ b/app/src/navigation/home.ts @@ -4,12 +4,11 @@ import type { NativeStackNavigationOptions } from '@react-navigation/native-stack'; +import { black } from '@selfxyz/mobile-sdk-alpha/constants/colors'; + import { HomeNavBar } from '@/components/navbar'; -import PointsScreen from '@/components/navbar/Points'; -import { PointsNavBar } from '@/components/navbar/PointsNavBar'; import ReferralScreen from '@/screens/app/ReferralScreen'; import HomeScreen from '@/screens/home/HomeScreen'; -import PointsInfoScreen from '@/screens/home/PointsInfoScreen'; import ProofHistoryDetailScreen from '@/screens/home/ProofHistoryDetailScreen'; import ProofHistoryScreen from '@/screens/home/ProofHistoryScreen'; @@ -22,14 +21,6 @@ const homeScreens = { presentation: 'card', } as NativeStackNavigationOptions, }, - Points: { - screen: PointsScreen, - options: { - title: 'Self Points', - header: PointsNavBar, - presentation: 'card', - } as NativeStackNavigationOptions, - }, Referral: { screen: ReferralScreen, options: { @@ -41,20 +32,14 @@ const homeScreens = { options: { title: 'Approved Requests', headerBackTitle: 'close', - }, + headerTintColor: black, + } as NativeStackNavigationOptions, }, ProofHistoryDetail: { screen: ProofHistoryDetailScreen, options: { title: 'Approval', - }, - }, - PointsInfo: { - screen: PointsInfoScreen, - options: { - headerBackTitle: 'close', - title: 'Self Points', - animation: 'slide_from_bottom', + headerTintColor: black, } as NativeStackNavigationOptions, }, }; diff --git a/app/src/navigation/types.ts b/app/src/navigation/types.ts index ee0f317f8..368004191 100644 --- a/app/src/navigation/types.ts +++ b/app/src/navigation/types.ts @@ -40,7 +40,6 @@ export type AccountRoutesParamList = { CloudBackupSettings: | { nextScreen?: 'SaveRecoveryPhrase'; - returnToScreen?: 'Points'; } | undefined; ProofSettings: undefined; @@ -58,9 +57,6 @@ export type AppRoutesParamList = { curveOrExponent?: string; }; Modal: ModalNavigationParams; - Gratification: { - points?: number; - }; StarfallPushCode: undefined; }; @@ -131,13 +127,6 @@ export type HomeRoutesParamList = { Home: { testReferralFlow?: boolean; }; - Points: undefined; - PointsInfo: - | { - showNextButton?: boolean; - callbackId?: number; - } - | undefined; }; /** diff --git a/app/src/providers/authProvider.tsx b/app/src/providers/authProvider.tsx index 64827fa2e..7f0faa769 100644 --- a/app/src/providers/authProvider.tsx +++ b/app/src/providers/authProvider.tsx @@ -419,8 +419,13 @@ export function getPrivateKeyFromMnemonic(mnemonic: string) { } export async function hasSecretStored() { - const seed = await Keychain.getGenericPassword({ service: SERVICE_NAME }); - return !!seed; + try { + const seed = await Keychain.getGenericPassword({ service: SERVICE_NAME }); + return !!seed; + } catch (error) { + console.warn('Error checking for stored secret:', error); + return false; + } } // Migrates existing mnemonic to use new security settings with accessControl. diff --git a/app/src/providers/selfClientProvider.tsx b/app/src/providers/selfClientProvider.tsx index 3eb3f6d2e..859677ec4 100644 --- a/app/src/providers/selfClientProvider.tsx +++ b/app/src/providers/selfClientProvider.tsx @@ -23,7 +23,7 @@ import { } from '@selfxyz/mobile-sdk-alpha'; import { logNFCEvent, logProofEvent } from '@/config/sentry'; -import { createSession, launchDidit } from '@/integrations/didit'; +import { createKycSession, launchKycVerification } from '@/integrations/kyc'; import type { RootStackParamList } from '@/navigation'; import { navigationRef } from '@/navigation'; import { @@ -316,7 +316,7 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => { documentTypes: string[]; }) => { currentCountryCode = countryCode; - // Store country code early so it's available for Didit fallback flows + // Store country code early so it's available for KYC fallback flows useMRZStore.getState().update({ countryCode }); navigateIfReady('IDPicker', { countryCode, documentTypes }); }, @@ -346,23 +346,25 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => { if ( useErrorInjectionStore .getState() - .shouldTrigger('didit_initialization') + .shouldTrigger('kyc_initialization') ) { - console.log('[DEV] Injecting Didit initialization error'); + console.log('[DEV] Injecting KYC initialization error'); throw new Error( - 'Injected Didit initialization error for testing', + 'Injected KYC initialization error for testing', ); } - const session = await createSession(); - const result = await launchDidit(session.sessionToken); + const session = await createKycSession(); + const result = await launchKycVerification( + session.sessionToken, + ); - console.log('[Didit] Result:', JSON.stringify(result)); + console.log('[KYC] Result type:', result.type); // User cancelled/dismissed without completing verification if (result.type === 'cancelled') { console.log( - '[Didit] User cancelled or closed without completing', + '[KYC] User cancelled or closed without completing', ); return; } @@ -370,7 +372,7 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => { // Dev-only: Check for injected verification error const shouldInjectVerificationError = useErrorInjectionStore .getState() - .shouldTrigger('didit_verification'); + .shouldTrigger('kyc_verification'); // Actual error from provider if ( @@ -378,7 +380,7 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => { shouldInjectVerificationError ) { if (shouldInjectVerificationError) { - console.log('[DEV] Injecting Didit verification error'); + console.log('[DEV] Injecting KYC verification error'); } else { const safeError = sanitizeErrorMessage( result.error?.message || @@ -400,7 +402,7 @@ export const SelfClientProvider = ({ children }: PropsWithChildren) => { // User completed verification // Navigate to KYC success screen console.log( - '[Didit] Verification submitted, status:', + '[KYC] Verification submitted, status:', result.session?.status, ); if (navigationRef.isReady()) { diff --git a/app/src/screens/account/recovery/AccountRecoveryChoiceScreen.tsx b/app/src/screens/account/recovery/AccountRecoveryChoiceScreen.tsx index fb83e318f..bef5e49fd 100644 --- a/app/src/screens/account/recovery/AccountRecoveryChoiceScreen.tsx +++ b/app/src/screens/account/recovery/AccountRecoveryChoiceScreen.tsx @@ -37,7 +37,8 @@ import { loadPassportData, reStorePassportDataWithRightCSCA, } from '@/providers/passportDataProvider'; -import { STORAGE_NAME, useBackupMnemonic } from '@/services/cloud-backup'; +import { recoveryCopy } from '@/screens/account/recovery/recoveryCopy'; +import { useBackupMnemonic } from '@/services/cloud-backup'; import { useSettingStore } from '@/stores/settingStore'; import type { Mnemonic } from '@/types/mnemonic'; @@ -82,25 +83,29 @@ const AccountRecoveryChoiceScreen: React.FC = () => { if (!result) { console.warn('Failed to restore account'); trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_UNKNOWN); - navigation.navigate({ name: 'Home', params: {} }); setRestoring(false); return false; } const passportData = await loadPassportData(); - const secret = getPrivateKeyFromMnemonic(mnemonic.phrase); - if (!passportData || !secret) { - console.warn('Failed to load passport data or secret'); - trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_AUTH, { - reason: 'no_passport_data_or_secret', + if (!passportData) { + console.warn( + 'Recovered secret but no local document data was found. Prompting the user to import their document again.', + ); + if (isCloudRestore && !cloudBackupEnabled) { + toggleCloudBackupEnabled(); + } + trackEvent(BackupEvents.CLOUD_RESTORE_SUCCESS, { + documentImportRequired: true, }); - navigation.navigate({ name: 'Home', params: {} }); + navigation.navigate('CountryPicker'); setRestoring(false); - return false; + return true; } const passportDataParsed = JSON.parse(passportData); + const secret = getPrivateKeyFromMnemonic(mnemonic.phrase); const { isRegistered, csca } = await isUserRegisteredWithAlternativeCSCA( @@ -140,7 +145,6 @@ const AccountRecoveryChoiceScreen: React.FC = () => { hasCSCA: !!csca, }, ); - navigation.navigate({ name: 'Home', params: {} }); setRestoring(false); return false; } @@ -241,16 +245,10 @@ const AccountRecoveryChoiceScreen: React.FC = () => { - Restore your Self account + {recoveryCopy.choice.title} - By continuing, you certify that this passport belongs to you and is - not stolen or forged.{' '} - {!biometricsAvailable && ( - <> - Your device doesn't support biometrics or is disabled for apps - and is required for cloud storage. - - )} + {recoveryCopy.choice.description}{' '} + {!biometricsAvailable && recoveryCopy.choice.noBiometrics} @@ -275,12 +273,11 @@ const AccountRecoveryChoiceScreen: React.FC = () => { testID="button-from-teststorage" disabled={restoringFromCloud || !biometricsAvailable} > - {restoringFromCloud ? 'Restoring' : 'Restore'} from {STORAGE_NAME} - {restoringFromCloud ? '…' : ''} + {recoveryCopy.choice.actions.cloud(restoringFromCloud)} - OR + {recoveryCopy.choice.actions.or} { - Enter recovery phrase + + {recoveryCopy.choice.actions.phrase} + diff --git a/app/src/screens/account/recovery/AccountRecoveryScreen.tsx b/app/src/screens/account/recovery/AccountRecoveryScreen.tsx index af7ef333a..c7b0cee5f 100644 --- a/app/src/screens/account/recovery/AccountRecoveryScreen.tsx +++ b/app/src/screens/account/recovery/AccountRecoveryScreen.tsx @@ -21,6 +21,7 @@ import { import RestoreAccountSvg from '@/assets/icons/restore_account.svg'; import useHapticNavigation from '@/hooks/useHapticNavigation'; import { ExpandableBottomLayout } from '@/layouts/ExpandableBottomLayout'; +import { recoveryCopy } from '@/screens/account/recovery/recoveryCopy'; const AccountRecoveryScreen: React.FC = () => { const onRestoreAccountPress = useHapticNavigation('AccountRecoveryChoice'); @@ -44,24 +45,21 @@ const AccountRecoveryScreen: React.FC = () => { - Restore your Self account - - By continuing, you certify that this passport belongs to you and is - not stolen or forged. - + {recoveryCopy.entry.title} + {recoveryCopy.entry.description} - Restore my account + {recoveryCopy.entry.actions.recover} - Create new account + {recoveryCopy.entry.actions.register} diff --git a/app/src/screens/account/recovery/DocumentDataNotFoundScreen.tsx b/app/src/screens/account/recovery/DocumentDataNotFoundScreen.tsx index 84baf5046..3291f3a66 100644 --- a/app/src/screens/account/recovery/DocumentDataNotFoundScreen.tsx +++ b/app/src/screens/account/recovery/DocumentDataNotFoundScreen.tsx @@ -46,7 +46,7 @@ const DocumentDataNotFoundScreen: React.FC = () => { - ✨ Are you new here? + No document found { color: slate200, }} > - It seems like you need to go through the registration flow first. + We couldn't find a registered document on this device. Register your + ID to continue. { height={150} backgroundColor={white} > - Go to Registration + Continue ); diff --git a/app/src/screens/account/recovery/RecoverWithPhraseScreen.tsx b/app/src/screens/account/recovery/RecoverWithPhraseScreen.tsx index a1a4e8f02..45760c2e1 100644 --- a/app/src/screens/account/recovery/RecoverWithPhraseScreen.tsx +++ b/app/src/screens/account/recovery/RecoverWithPhraseScreen.tsx @@ -36,6 +36,7 @@ import { loadPassportData, reStorePassportDataWithRightCSCA, } from '@/providers/passportDataProvider'; +import { recoveryCopy } from '@/screens/account/recovery/recoveryCopy'; const RecoverWithPhraseScreen: React.FC = () => { const navigation = @@ -70,26 +71,22 @@ const RecoverWithPhraseScreen: React.FC = () => { trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_AUTH, { mnemonicLength: slimMnemonic.split(' ').length, }); - navigation.navigate({ name: 'Home', params: {} }); setRestoring(false); return; } const passportData = await loadPassportData(); - const secret = getPrivateKeyFromMnemonic(slimMnemonic); - if (!passportData || !secret) { + if (!passportData) { console.warn( - 'No passport data found on device. Please scan or import your document.', + 'Recovered secret but no local document data was found. Prompting the user to import their document again.', ); - trackEvent(BackupEvents.CLOUD_RESTORE_FAILED_AUTH, { - reason: 'no_passport_data', - }); - navigation.navigate({ name: 'Home', params: {} }); + navigation.navigate('CountryPicker'); setRestoring(false); return; } const passportDataParsed = JSON.parse(passportData); + const secret = getPrivateKeyFromMnemonic(slimMnemonic); const { isRegistered, csca } = await isUserRegisteredWithAlternativeCSCA( passportDataParsed, @@ -124,7 +121,6 @@ const RecoverWithPhraseScreen: React.FC = () => { reason: 'document_not_registered', hasCSCA: !!csca, }); - navigation.navigate({ name: 'Home', params: {} }); setRestoring(false); return; } @@ -143,7 +139,6 @@ const RecoverWithPhraseScreen: React.FC = () => { error: error instanceof Error ? error.message : 'unknown', }); setRestoring(false); - navigation.navigate({ name: 'Home', params: {} }); } }, [ mnemonic, @@ -162,8 +157,7 @@ const RecoverWithPhraseScreen: React.FC = () => { style={styles.layout} > - Your recovery phrase has 24 words. Enter the words in the correct order, - separated by spaces. + {recoveryCopy.phrase.instructions}