diff --git a/.github/workflows/e2e-android.yml b/.github/workflows/e2e-android.yml new file mode 100644 index 00000000..a8974b0e --- /dev/null +++ b/.github/workflows/e2e-android.yml @@ -0,0 +1,102 @@ +name: E2E Android + +on: + workflow_dispatch: + schedule: + # 2h00 UTC chaque nuit + - cron: '0 2 * * *' + push: + branches: + - feat/sdk-v6-migration + paths: + - 'packages/purchasely/android/build.gradle' + - 'packages/*/android/build.gradle' + - '.github/workflows/e2e-android.yml' + - 'integration_test/**' + - 'example/src/E2ETestRunner.tsx' + - 'example/android/app/build.gradle' + +concurrency: + group: e2e-android + cancel-in-progress: true + +jobs: + e2e-android: + name: E2E Tests (Android T1-T13) + # ubuntu-latest: KVM disponible pour x86_64, requis par reactivecircuit/android-emulator-runner. + # macOS sera utilisé dans e2e-ios.yml (simulateur iOS). + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Setup Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'yarn' + + - name: Setup Java 17 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v5 + with: + gradle-version: wrapper + cache-read-only: false + + - name: Install JS dependencies + run: yarn install --immutable + + - name: Build JS SDK + run: yarn purchasely:prepare + + - name: Build release APK (JS always bundled in release) + # Release builds always embed the JS bundle — no Metro bundler needed in CI. + # ProGuard is disabled in build.gradle (enableProguardInReleaseBuilds = false). + # -x lintVitalRelease skips lint step that false-positives on ReactActivity. + env: + JAVA_OPTS: "-XX:MaxHeapSize=4g" + run: | + cd example/android + ./gradlew :app:assembleRelease -x lintVitalRelease + # Verify the JS bundle is embedded + APK="app/build/outputs/apk/release/app-release.apk" + if unzip -l "$APK" | grep -q "index.android.bundle"; then + echo "✅ JS bundle confirmed in APK" + else + echo "❌ JS bundle NOT found in APK" + exit 1 + fi + + - name: Run E2E suite on emulator + # ReactiveCircus/android-emulator-runner handles: system-image install, + # AVD creation, emulator start with KVM, boot wait, animations disable. + uses: ReactiveCircus/android-emulator-runner@v2 + with: + api-level: 34 + arch: x86_64 + target: google_apis + avd-name: rn_e2e_api34 + disable-animations: true + emulator-options: -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect -no-snapshot + script: bash integration_test/run_e2e.sh emulator-5554 --skip-build + + - name: Upload logcat on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: logcat-${{ github.run_id }} + path: /tmp/e2e_rn_logcat_*.log + retention-days: 7 diff --git a/.github/workflows/e2e-ios.yml b/.github/workflows/e2e-ios.yml new file mode 100644 index 00000000..8dae1a78 --- /dev/null +++ b/.github/workflows/e2e-ios.yml @@ -0,0 +1,107 @@ +name: E2E iOS + +on: + workflow_dispatch: + schedule: + # 2h30 UTC chaque nuit (décalé de l'E2E Android à 2h00) + - cron: '30 2 * * *' + push: + branches: + - feat/sdk-v6-migration + paths: + - 'packages/purchasely/react-native-purchasely.podspec' + - 'packages/purchasely/ios/**' + - '.github/workflows/e2e-ios.yml' + - 'integration_test/**' + - 'example/src/E2ETestRunner.tsx' + - 'example/ios/**' + +concurrency: + group: e2e-ios + cancel-in-progress: true + +jobs: + e2e-ios: + name: E2E Tests (iOS T1-T13) + # macOS requis : simulateur iOS + xcodebuild. Pas de HVF nécessaire (le + # simulateur iOS ne virtualise pas un OS invité, contrairement à l'émulateur + # Android ARM — c'est pourquoi Android tourne sur ubuntu + KVM). + # macos-15 : Xcode 16.x par défaut (RN 0.79 exige Xcode >= 16.1). + runs-on: macos-15 + timeout-minutes: 60 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Select latest Xcode 16 (RN 0.79 requires >= 16.1) + run: | + LATEST=$(ls -d /Applications/Xcode_16*.app 2>/dev/null | sort -V | tail -1) + if [ -n "$LATEST" ]; then sudo xcode-select -s "$LATEST/Contents/Developer"; fi + xcodebuild -version + + - name: Setup (Node + Yarn) + uses: ./.github/actions/setup + + - name: Build JS SDK + run: yarn prepare + + - name: Cache CocoaPods + uses: actions/cache@v4 + with: + path: | + example/ios/Pods + ~/.cocoapods + ~/Library/Caches/CocoaPods + key: ${{ runner.os }}-cocoapods-${{ hashFiles('example/ios/Podfile.lock', 'packages/purchasely/react-native-purchasely.podspec') }} + restore-keys: | + ${{ runner.os }}-cocoapods- + + - name: Install CocoaPods + run: | + cd example/ios + pod install --repo-update + env: + NO_FLIPPER: 1 + + - name: Install idb (companion + client) + run: | + # idb_companion : tap Facebook (nécessite brew trust sur les brew récents). + brew tap facebook/fb + brew trust facebook/fb || true + brew install idb-companion + # fb-idb (client Python) ne supporte pas Python 3.14 (asyncio.get_event_loop + # y lève) — on l'isole dans un venv 3.11/3.12/3.13 déterministe et on + # exporte le binaire via $IDB (lu par les drivers tap/swipe). + PY=$(command -v python3.12 || command -v python3.11 || command -v python3.13 || command -v python3) + "$PY" -m venv "$RUNNER_TEMP/idbvenv" + "$RUNNER_TEMP/idbvenv/bin/pip" install -q --upgrade pip fb-idb + echo "IDB=$RUNNER_TEMP/idbvenv/bin/idb" >> "$GITHUB_ENV" + "$RUNNER_TEMP/idbvenv/bin/idb" --help >/dev/null && echo "✅ idb client OK" + + - name: Boot iOS simulator + run: | + UDID=$(xcrun simctl list devices available -j | python3 -c " + import sys, json + d = json.load(sys.stdin)['devices'] + cands = [v for k, vs in d.items() if 'iOS' in k for v in vs if 'iPhone' in v['name']] + # iPhone 15/16 de préférence, sinon le dernier iPhone disponible + pref = [c for c in cands if any(n in c['name'] for n in ('iPhone 16', 'iPhone 15'))] + chosen = (pref or cands)[-1] + print(chosen['udid']) + ") + echo "Booting simulator $UDID" + xcrun simctl boot "$UDID" + xcrun simctl bootstatus "$UDID" -b + echo "IOS_SIMULATOR_UDID=$UDID" >> "$GITHUB_ENV" + + - name: Run E2E suite on simulator (build Release + T1-T13) + run: bash integration_test/run_e2e_ios.sh "$IOS_SIMULATOR_UDID" + + - name: Upload logs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: ios-e2e-logs-${{ github.run_id }} + path: /tmp/e2e_rn_ios_*.log + retention-days: 7 diff --git a/.gitignore b/.gitignore index b0e5923c..0d94b3d5 100644 --- a/.gitignore +++ b/.gitignore @@ -88,4 +88,7 @@ nitrogen/ .nx/cache -.nx/workspace-data \ No newline at end of file +.nx/workspace-data +jest_dx/ +node-compile-cache/ +**/coverage/ diff --git a/.nvmrc b/.nvmrc index 9a2a0e21..53d1c14d 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20 +v22 diff --git a/CLAUDE.md b/CLAUDE.md index 07f5ffa8..0c1deaaa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,13 +11,13 @@ | Property | Value | |----------|-------| -| Current Version | 5.7.3 | +| Current Version | 6.0.0-rc.1 | | React Native | 0.79.2 | | TypeScript | 5.2.2 (strict mode) | | Node.js | v20 (see `.nvmrc`) | | Package Manager | Yarn 3.6.1 (workspaces) | -| Native iOS SDK | 5.7.4 | -| Native Android SDK | 5.7.4 | +| Native iOS SDK | 6.0.0 | +| Native Android SDK | 6.0.0 | ### Supported App Stores - Apple App Store (iOS) @@ -231,39 +231,56 @@ The following sections provide quick API examples. For comprehensive documentati Refer to the [SDK Public Documentation](../sdk_public_doc.md). -### Initialization +> **v6 paywall API only.** The v5 paywall methods (`start({...})`, +> `presentPresentationForPlacement`, `presentPresentationWithIdentifier`, +> `presentProductWithIdentifier`, `presentPlanWithIdentifier`, +> `fetchPresentation`, `setPaywallActionInterceptorCallback`, `onProcessAction`, +> `setDefaultPresentationResultCallback`, `readyToOpenDeeplink`, …) are +> **removed**. Use the builders below. Full mapping: `MIGRATION-v6.md`. + +### Initialization (v6 builder) ```typescript -import Purchasely, { LogLevels, RunningMode } from 'react-native-purchasely' - -await Purchasely.start({ - apiKey: 'YOUR_API_KEY', - androidStores: ['Google'], // or ['Huawei', 'Amazon'] - storeKit1: false, // iOS: use StoreKit 2 - userId: 'user_id', // optional - logLevel: LogLevels.DEBUG, - runningMode: RunningMode.FULL -}) +import Purchasely from 'react-native-purchasely' + +await Purchasely.builder('YOUR_API_KEY') + .appUserId('user_id') // optional + .runningMode('full') // 'observer' (default) | 'full' + .logLevel('debug') // 'debug' | 'info' | 'warn' | 'error' + .allowDeeplink(true) // replaces readyToOpenDeeplink(true) + .stores(['google']) // Android only: 'google' | 'huawei' | 'amazon' + .storekitVersion('storeKit2') // iOS only: 'storeKit1' | 'storeKit2' + .start() ``` -### Presentation Methods +### Presentation Methods (v6 builders) + +`Purchasely.presentation` is the `PresentationBuilder`. `build()` returns a +`PresentationRequest`; `display()` resolves at dismiss with a 5-field +`PresentationOutcome` (`{ presentation, purchaseResult, plan, closeReason, +error }`). ```typescript -// Fetch presentation data -const presentation = await Purchasely.fetchPresentation({ - placementVendorId: 'ONBOARDING', - contentId: 'content_123' -}) +// Preload a placement (was fetchPresentation) +const request = Purchasely.presentation.placement('ONBOARDING').build() +const presentation = await request.preload() -// Present full-screen paywall -const result = await Purchasely.presentPresentationForPlacement({ - placementVendorId: 'ONBOARDING', - isFullscreen: true -}) +// Present a placement full-screen (was presentPresentationForPlacement) +const outcome = await Purchasely.presentation.placement('ONBOARDING').build().display() -// Present specific product or plan -await Purchasely.presentProductWithIdentifier('product_id') -await Purchasely.presentPlanWithIdentifier('plan_id') +// Present a specific screen (was presentPresentationWithIdentifier) +await Purchasely.presentation.screen('SCREEN_ID').build().display() + +// Present a specific product / plan (was presentProductWithIdentifier / presentPlanWithIdentifier) +await Purchasely.presentation.screen('SCREEN_ID').contentId('CONTENT_ID').build().display() + +// Lifecycle: request.display() (show) / request.close() (hide) / request.back() + +// Action interception (was setPaywallActionInterceptorCallback + onProcessAction) +Purchasely.interceptAction('purchase', async (info, payload) => { + // return 'success' | 'failed' | 'notHandled' + return 'notHandled' +}) ``` ### Event Listening @@ -388,13 +405,13 @@ Build orchestration with caching: ### Native Dependencies **iOS (CocoaPods):** -- Purchasely SDK v5.7.4 +- Purchasely SDK v6.0.0 - Deployment target: iOS 13.4 **Android (Gradle):** -- io.purchasely:core:5.7.4 +- io.purchasely:core:6.0.0 - Min SDK: 21 -- Kotlin: 1.9+ +- Kotlin: 2.1+ - Java: 11 --- diff --git a/MIGRATION-v6.md b/MIGRATION-v6.md new file mode 100644 index 00000000..5bf57eac --- /dev/null +++ b/MIGRATION-v6.md @@ -0,0 +1,374 @@ +# Migrating to Purchasely React Native SDK v6 + +Purchasely React Native SDK **v6 is paywall-API-only**: the legacy v5 paywall +API has been **REMOVED** (not deprecated). Calling any of the removed methods +will fail to compile (TypeScript) and the methods no longer exist at runtime. + +This guide maps every removed v5 paywall method to its v6 replacement and lists +the methods that are **unchanged**. + +> **Tip — let the AI help you migrate.** The Purchasely AI plugin and the +> `purchasely-integrate`, `purchasely-review` and `purchasely-debug` skills can +> read your integration and rewrite the v5 paywall calls to the v6 builder API +> for you. Point them at the files that call `Purchasely.start`, +> `presentPresentationForPlacement`, `fetchPresentation`, +> `setPaywallActionInterceptorCallback`, etc. + +--- + +## TL;DR + +- The paywall surface is now built around three entry points exposed on the + `Purchasely` default export: + - `Purchasely.builder(apiKey)` — chainable SDK start. + - `Purchasely.presentation` — the `PresentationBuilder` (`.placement(id)`, + `.screen(id)`, `.default()`). + - `Purchasely.interceptAction(kind, handler)` — typed action interception. +- `PresentationBuilder.build()` returns a **`PresentationRequest`** with a + lifecycle (`preload()`, `display(transition?)`, `close()`, `back()`). +- `display()` resolves at **dismiss** with a 5-field `PLYPresentationOutcome` + (`{ presentation, purchaseResult, plan, closeReason, error }`). +- **All CORE methods are UNCHANGED** — see [Unchanged](#whats-unchanged). + +--- + +## Removed v5 paywall API → v6 replacement + +| Removed v5 method | v6 replacement | +|-------------------|----------------| +| `Purchasely.start({ apiKey, androidStores, storeKit1, userId, logLevel, runningMode })` | `Purchasely.builder(apiKey).appUserId(userId).runningMode('full').logLevel('error').stores(['google']).storekitVersion('storeKit2').start()` | +| `Purchasely.startWithAPIKey(apiKey, stores, userId, logLevel, runningMode)` | `Purchasely.builder(apiKey).appUserId(userId).runningMode('full').start()` | +| `Purchasely.fetchPresentation({ placementId })` | `Purchasely.presentation.placement(id).build().preload()` | +| `Purchasely.presentPresentationForPlacement({ placementVendorId })` | `Purchasely.presentation.placement(id).build().display()` | +| `Purchasely.presentPresentationWithIdentifier({ presentationVendorId })` | `Purchasely.presentation.screen(id).build().display()` | +| `Purchasely.presentPresentation({ presentation })` | preload then display the same request: `const req = Purchasely.presentation.placement(id).build(); await req.preload(); await req.display()` | +| `Purchasely.presentProductWithIdentifier(productId, …)` | `Purchasely.presentation.screen(id).contentId(contentId).build().display()` | +| `Purchasely.presentPlanWithIdentifier(planId, …)` | `Purchasely.presentation.screen(id).build().display()` | +| `Purchasely.showPresentation()` / `Purchasely.presentPresentation(...)` | request lifecycle: `request.display()` | +| `Purchasely.hidePresentation()` / `Purchasely.closePresentation()` | request lifecycle: `request.close()` | +| `Purchasely.setPaywallActionInterceptorCallback(cb)` + `Purchasely.onProcessAction(bool)` | `Purchasely.interceptAction(kind, handler)` — handler returns `'success' \| 'failed' \| 'notHandled'` (no more `onProcessAction`) | +| `Purchasely.setDefaultPresentationResultCallback(cb)` / `setDefaultPresentationResultHandler(cb)` | `Purchasely.setDefaultPresentationDismissHandler(outcome => …)` — global handler for presentations the SDK opens itself (campaigns, deeplinks, Promoted IAP). For paywalls **you** display, use `request.onDismissed(outcome => …)` instead. | +| `Purchasely.readyToOpenDeeplink(true)` | `Purchasely.builder(apiKey).allowDeeplink(true).start()` | + +--- + +## Initialization + +### Before (v5 — removed) + +```typescript +import Purchasely, { LogLevels, RunningMode } from 'react-native-purchasely' + +await Purchasely.start({ + apiKey: 'YOUR_API_KEY', + androidStores: ['Google'], + storeKit1: false, + userId: 'user_id', + logLevel: LogLevels.ERROR, + runningMode: RunningMode.FULL, +}) + +Purchasely.readyToOpenDeeplink(true) +``` + +### After (v6) + +```typescript +import Purchasely from 'react-native-purchasely' + +const configured = await Purchasely.builder('YOUR_API_KEY') + .appUserId('user_id') // optional, defaults to anonymous + .runningMode('full') // 'observer' (default) | 'full' + .logLevel('error') // 'debug' | 'info' | 'warn' | 'error' + .allowDeeplink(true) // replaces readyToOpenDeeplink(true) + .allowCampaigns(true) // automatic campaigns + .stores(['google']) // Android only: 'google' | 'huawei' | 'amazon' + .storekitVersion('storeKit2')// iOS only: 'storeKit1' | 'storeKit2' + .start() +``` + +> **⚠️ Major breaking change — the default `runningMode` is now `'observer'` +> (v5 effectively defaulted to `full`).** This is a **silent behavioural change**: +> it does **not** produce a compile error, so an app that previously let +> Purchasely own the purchase flow will **stop doing so** after upgrading unless +> it explicitly passes `.runningMode('full')`. Audit every `start()`/`builder()` +> call. The change is consistent across platforms (iOS, Android, Flutter, React +> Native), including the native fallback: any unknown/unset value now resolves to +> `observer`, never `full`. + +--- + +## Displaying a paywall + +### Before (v5 — removed) + +```typescript +const result = await Purchasely.presentPresentationForPlacement({ + placementVendorId: 'ONBOARDING', + contentId: 'my_content_id', + isFullscreen: true, +}) + +switch (result.result) { + case ProductResult.PRODUCT_RESULT_PURCHASED: + case ProductResult.PRODUCT_RESULT_RESTORED: + console.log('Purchased', result.plan?.name) + break + case ProductResult.PRODUCT_RESULT_CANCELLED: + break +} +``` + +### After (v6) + +`display()` resolves at **dismiss** with a `PLYPresentationOutcome`: + +```typescript +const outcome = await Purchasely.presentation + .placement('ONBOARDING') + .contentId('my_content_id') + .build() + .display() + +// outcome: { presentation, purchaseResult, plan, closeReason, error } +if (outcome.error) { + console.error(outcome.error.message) +} else if (outcome.purchaseResult === 'purchased' || outcome.purchaseResult === 'restored') { + console.log('Purchased', outcome.plan?.name) +} else { + console.log('Dismissed', outcome.closeReason) // 'button' | 'backSystem' | 'programmatic' +} +``` + +`purchaseResult` is now a string union (`'purchased' | 'cancelled' | 'restored'`) +instead of the `ProductResult` ordinal enum. + +### Targeting a specific screen / product / plan + +```typescript +// Specific presentation by screen id (was presentPresentationWithIdentifier) +await Purchasely.presentation.screen('SCREEN_ID').build().display() + +// Specific product (was presentProductWithIdentifier) +await Purchasely.presentation.screen('SCREEN_ID').contentId('CONTENT_ID').build().display() + +// Specific plan (was presentPlanWithIdentifier) +await Purchasely.presentation.screen('SCREEN_ID').build().display() +``` + +--- + +## Pre-fetching (preload) + +### Before (v5 — removed) + +```typescript +const presentation = await Purchasely.fetchPresentation({ placementId: 'ONBOARDING' }) +const result = await Purchasely.presentPresentation({ presentation }) +``` + +### After (v6) + +```typescript +const request = Purchasely.presentation.placement('ONBOARDING').build() +const presentation = await request.preload() // resolves when the screen is loaded +// later, when ready to show it: +const outcome = await request.display() +``` + +--- + +## Presentation lifecycle (show / hide / close) + +The imperative `showPresentation` / `hidePresentation` / `closePresentation` +methods are replaced by the request lifecycle: + +```typescript +const request = Purchasely.presentation.placement('ONBOARDING').build() + +request.display() // show +request.close() // hide / close +request.back() // navigate back inside a multi-step (Flow) presentation +``` + +> `request.close()` currently dismisses **all** displayed presentations (the +> native SDK does not yet expose a per-request close). If you stack +> presentations, closing one will dismiss the others. + +--- + +## Action interceptor + +`setPaywallActionInterceptorCallback` + `onProcessAction` are replaced by +`Purchasely.interceptAction(kind, handler)`. Register **one handler per action +kind**; the handler returns `'success' | 'failed' | 'notHandled'` instead of +calling `onProcessAction(true/false)`. + +### Before (v5 — removed) + +```typescript +Purchasely.setPaywallActionInterceptorCallback((result) => { + if (result.action === PLYPaywallAction.PURCHASE) { + MyPurchaseSystem.purchase(result.parameters.plan.productId) + Purchasely.onProcessAction(false) + } else { + Purchasely.onProcessAction(true) + } +}) +``` + +### After (v6) + +```typescript +import { Linking } from 'react-native' + +Purchasely.interceptAction('purchase', async (info, payload) => { + if (payload?.kind === 'purchase') { + const ok = await MyPurchaseSystem.purchase(payload.plan.productId) + return ok ? 'success' : 'failed' + } + return 'notHandled' +}) + +Purchasely.interceptAction('navigate', async (info, payload) => { + if (payload?.kind === 'navigate') { + Linking.openURL(payload.url) + return 'success' + } + return 'notHandled' +}) + +// Cleanup +Purchasely.removeActionInterceptor('purchase') +Purchasely.removeAllActionInterceptors() +``` + +Known action kinds: `close`, `closeAll`, `login`, `navigate`, `purchase`, +`restore`, `openPresentation`, `openPlacement`, `promoCode`, `webCheckout`. + +--- + +## Deeplinks, campaigns & the default dismiss handler + +```typescript +// Allow deeplinks (replaces readyToOpenDeeplink(true)) — set at start: +await Purchasely.builder('YOUR_API_KEY').allowDeeplink(true).start() +``` + +There are **two distinct paywall flows** — don't conflate them: + +### 1. Paywalls **you** display + +When your app instantiates the presentation, read the result from that request +(`await display()` or `request.onDismissed(...)`): + +```typescript +const outcome = await Purchasely.presentation.placement('ONBOARDING').build().display() +``` + +### 2. Paywalls the **SDK** opens itself (campaigns, deeplinks, Promoted IAP) + +Your app never calls `display()` for these, so there is no request to attach a +callback to. Register the **global default dismiss handler** instead. It is the +v6 replacement for `setDefaultPresentationResultCallback` / +`setDefaultPresentationResultHandler`, and mirrors the native +`Purchasely.setDefaultPresentationDismissHandler`: + +```typescript +import Purchasely from 'react-native-purchasely' + +const subscription = Purchasely.setDefaultPresentationDismissHandler((outcome) => { + // outcome: { presentation, purchaseResult, plan, closeReason, error } + // `presentation` is always populated here — use it to tell which + // campaign/deeplink screen closed. + console.log( + 'SDK paywall dismissed:', + outcome.presentation?.screenId, + outcome.purchaseResult, // 'purchased' | 'restored' | 'cancelled' | null + outcome.closeReason // 'button' | 'backSystem' | 'programmatic' | null + ) +}) + +// Only one handler is active at a time — calling again replaces it. +// Remove it (e.g. on unmount) with either: +subscription.remove() +// …or: +Purchasely.removeDefaultPresentationDismissHandler() +``` + +> **Platform note.** `closeReason` mirrors the native `PLYCloseReason` +> (`button` / `backSystem` / `programmatic`) and is `null` when the SDK does not +> report a reason. The iOS interactive dismiss (swipe-down / nav pop) maps to +> `backSystem` for parity with Android's system back. + +```typescript +// `isDeeplinkHandled` was RENAMED to `handleDeeplink` (matches the native SDK): +const handled = await Purchasely.handleDeeplink('app://ply/presentations/') +``` + +--- + +## Synchronize (now awaitable) + +`Purchasely.synchronize()` previously returned `void` (fire-and-forget). The v6 +native SDKs expose completion callbacks (iOS `synchronize(success:failure:)`, +Android `synchronize(onSuccess:(PLYPlan?)->Unit, onError:(PLYError?)->Unit)`), +so the bridge now returns a **`Promise`** that resolves when the +receipt synchronization completes and rejects on failure. + +This is **source-compatible**: existing fire-and-forget callers keep working +(they just ignore the returned promise). New code can await it: + +```typescript +try { + await Purchasely.synchronize() // resolves when the sync finishes + console.log('Synchronized') +} catch (e) { + console.error('Synchronize failed', e) // e.g. PLYError.NoStoreConfigured +} +``` + +> In Observer mode after a host-side purchase, `await Purchasely.synchronize()` +> before chaining a follow-up placement so the receipt is uploaded first. + +--- + +## What's UNCHANGED + +All **core** SDK methods are unchanged in name, signature, and behaviour. Only +the v5 *paywall* surface was removed (plus `synchronize`, which gained an +awaitable result — see above). The following keep working exactly as in v5: + +- **User**: `userLogin`, `userLogout`, `getAnonymousUserId`, `isAnonymous`. +- **Products**: `allProducts`, `productWithIdentifier`, `planWithIdentifier`, + `purchaseWithPlanVendorId`, `signPromotionalOffer`, `isEligibleForIntroOffer`, + `setDynamicOffering`, `getDynamicOfferings`, `removeDynamicOffering`, + `clearDynamicOfferings`. +- **Subscriptions data**: `userSubscriptions`, `userSubscriptionsHistory`, + `restoreAllProducts`, `silentRestoreAllProducts`, + `userDidConsumeSubscriptionContent`. + +> **Removed:** `presentSubscriptions()` no longer exists (iOS **and** Android). +> The native v6 SDKs dropped the built-in subscription-list UI — build your own +> screen from `userSubscriptions()` / `userSubscriptionsHistory()`. +- **Attributes**: `setUserAttributeWith{String,Number,Boolean,Date,StringArray,NumberArray,BooleanArray}`, + `incrementUserAttribute`, `decrementUserAttribute`, `userAttributes`, + `userAttribute`, `clearUserAttribute`, `clearUserAttributes`, + `clearBuiltInAttributes`, `setAttribute`. +- **Listeners**: `addEventListener` / `removeEventListener`, + `addPurchasedListener` / `removePurchasedListener`, + `addUserAttributeSetListener` / `removeUserAttributeSetListener`, + `addUserAttributeRemovedListener` / `removeUserAttributeRemovedListener`. +- **Client (BYOS) presentations**: **`clientPresentationDisplayed`**, + **`clientPresentationClosed`** — unchanged. +- **Misc**: `setLogLevel`, `setLanguage`, `setThemeMode`, `setDebugMode`, + `revokeDataProcessingConsent`, `getConstants`, `close`. +- **Embedded component**: `PLYPresentationView` — unchanged. + +--- + +## Need a hand? + +Use the Purchasely AI plugin / skills (`purchasely-integrate`, +`purchasely-review`, `purchasely-debug`) to scan your project and apply this +migration automatically. diff --git a/README.md b/README.md index 5f926cf0..588d65d1 100644 --- a/README.md +++ b/README.md @@ -10,31 +10,32 @@ npm install react-native-purchasely ## 🔧 Setup +> **v6** — the SDK is initialized and paywalls are displayed with the chainable +> builder API. The legacy v5 paywall API (`start({...})`, `startWithAPIKey`, +> `presentPresentationForPlacement`, `fetchPresentation`, +> `setPaywallActionInterceptorCallback`, …) has been **removed**. See +> [`MIGRATION-v6.md`](./MIGRATION-v6.md) for the full old→new mapping. + Add the following code in the root of your project (typically `App.tsx` in a React Native project): ```ts -import Purchasely, { LogLevels, RunningMode } from 'react-native-purchasely' - -Purchasely.startWithAPIKey( - 'afa96c76-1d8e-4e3c-a48f-204a3cd93a15', - ['Google'], // List of stores for Android, accepted values: Google, Huawei, and Amazon - null, // Your user ID - LogLevels.DEBUG, // Log level, should be warning or error in production - RunningMode.FULL // Running mode -).then( - (configured) => { - if (!configured) { - console.log('Purchasely SDK not properly initialized') - return - } - - console.log('Purchasely SDK is initialized') - setupPurchasely() - }, - (error) => { - console.log('Purchasely SDK initialization error', error) - } -) +import Purchasely from 'react-native-purchasely' + +const configured = await Purchasely.builder('afa96c76-1d8e-4e3c-a48f-204a3cd93a15') + .stores(['google']) // Android stores: 'google' | 'huawei' | 'amazon' + .appUserId(null) // your user ID, or null for anonymous + .logLevel('debug') // 'warn' or 'error' in production + .runningMode('full') // 'observer' (default) | 'full' + .allowDeeplink(true) + .storekitVersion('storeKit2') // iOS only + .start() + +if (!configured) { + console.log('Purchasely SDK not properly initialized') +} else { + console.log('Purchasely SDK is initialized') + setupPurchasely() +} ``` ## 🎬 Usage @@ -42,28 +43,25 @@ Purchasely.startWithAPIKey( ### 1️⃣ Full Screen Paywall ```ts -import Purchasely, { - PLYPresentationType, - ProductResult, -} from 'react-native-purchasely' +import Purchasely from 'react-native-purchasely' try { - const result = await Purchasely.presentPresentationForPlacement({ - placementVendorId: 'composer', - loadingBackgroundColor: '#FFFFFFFF', - }) - - console.log('Result is ' + result.result) - - switch (result.result) { - case ProductResult.PRODUCT_RESULT_PURCHASED: - case ProductResult.PRODUCT_RESULT_RESTORED: - if (result.plan != null) { - console.log('User purchased ' + result.plan.name) - } - break - case ProductResult.PRODUCT_RESULT_CANCELLED: - break + // display() resolves at dismiss with a PresentationOutcome + const outcome = await Purchasely.presentation + .placement('composer') + .backgroundColor('#FFFFFFFF') + .build() + .display() + + if (outcome.error) { + console.error(outcome.error.message) + } else if ( + outcome.purchaseResult === 'purchased' || + outcome.purchaseResult === 'restored' + ) { + console.log('User purchased ' + outcome.plan?.name) + } else { + console.log('Dismissed: ' + outcome.closeReason) } } catch (e) { console.error(e) @@ -72,71 +70,29 @@ try { ### 2️⃣ Nested View Paywall +The embedded `PLYPresentationView` component is part of the **core** API and is +unchanged in v6. Pass a `placementId` directly — no manual pre-fetch step is +required. + ```ts import { Text, View } from 'react-native'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { Header } from 'react-native/Libraries/NewAppScreen'; import { Section } from './Section.tsx'; -import Purchasely, { - PLYPresentationView, - PresentPresentationResult, - ProductResult, - PurchaselyPresentation, -} from 'react-native-purchasely'; -import { useEffect, useState } from 'react'; +import { PLYPresentationView, PresentPresentationResult } from 'react-native-purchasely'; export const PaywallScreen: React.FC> = ({ navigation }) => { - const [purchaselyPresentation, setPurchaselyPresentation] = useState(); - - useEffect(() => { - fetchPresentation(); - }, []); - - const fetchPresentation = async () => { - try { - setPurchaselyPresentation( - await Purchasely.fetchPresentation({ - placementId: 'ONBOARDING', - contentId: null, - }) - ); - } catch (e) { - console.error(e); - } - }; - const callback = (result: PresentPresentationResult) => { - console.log('### Paywall closed'); - console.log('### Result is ' + result.result); - switch (result.result) { - case ProductResult.PRODUCT_RESULT_PURCHASED: - case ProductResult.PRODUCT_RESULT_RESTORED: - if (result.plan != null) { - console.log('User purchased ' + result.plan.name); - } - break; - case ProductResult.PRODUCT_RESULT_CANCELLED: - console.log('User cancelled'); - break; - } + console.log('### Paywall closed, result is ' + result.result); navigation.goBack(); }; - if (purchaselyPresentation == null) { - return ( - - Loading ... - - ); - } - return (
callback(res)} /> @@ -153,6 +109,9 @@ export const PaywallScreen: React.FC> = ({ navigatio A complete documentation is available on our website: [Purchasely Docs](https://docs.purchasely.com/quick-start/sdk-installation/react-native-sdk). +Migrating from v5? See [`MIGRATION-v6.md`](./MIGRATION-v6.md) for the complete +old→new mapping of every removed v5 paywall method. + ## 🛠️ Developer Guide ### 1️⃣ Clone the Repository diff --git a/V6_MIGRATION_REPORT.md b/V6_MIGRATION_REPORT.md new file mode 100644 index 00000000..f6afc07c --- /dev/null +++ b/V6_MIGRATION_REPORT.md @@ -0,0 +1,370 @@ +# Rapport de migration React Native → Purchasely SDK natif 6.0 + +> Session du 2026-06-16 sur la branche `feat/sdk-v6-migration`. +> Ce document récapitule **tout ce qui a été fait** pour finaliser la migration +> du SDK React Native vers les SDK natifs Purchasely 6.0, et sert de source pour +> mettre à jour `../Documentation` (docs publiques) et `../purchasely-ai-skill` +> (références `react-native/`), comme cela a été fait pour Android, iOS et Flutter. + +> **⚠️ Mise à jour 2026-06-16 (session suivante — bridge iOS finalisé).** +> Les **doutes n°2 et n°7** ci-dessous sont **obsolètes**. Le bridge iOS ne +> compilait en réalité **pas** contre le pod `6.0.0-rc.1` : il appelait des API +> natives **v5 supprimées** (`presentationController(for:…)`, `fetchPresentationFor:`, +> `fetchPresentationWith:`, `closeDisplayedPresentation`, `subscriptionsController`), +> erreurs masquées par l'échec `fmt`. Corrections appliquées cette session : +> - **`PurchaselyRN.m` / `PurchaselyView.swift`** migrés vers l'**API builder v6** : +> `PLYPresentationBuilder.forPlacementId(_:)`/`forScreenId(_:)` → `build()` → +> `preloadWithCompletion:` + `onDismissed:` (`PLYPresentationOutcome`), +> `showController:type:from:` conservé, `closeDisplayedPresentation` → +> `[presentation close]` / `closeAllScreens`. +> - **RunningMode** : `TransactionOnly`/`PaywallObserver` supprimés du natif (reste +> `Observer=2`/`Full=3`) → enum local `PLYRNRunningMode` + `runningModeFromOrdinal()`. +> Fallback (valeur inconnue/non définie) → **Observer** (défaut v6, iso Flutter) ; +> seul `full` engage le flux d'achat Purchasely. +> - `setDynamicOffering` : nouveau paramètre `billingPlanType:`. +> - Bug ObjC préexistant masqué : `PLYPresentationPlan+Hybrid.m` `self.default` +> (mot-clé réservé) → `self.default_`. +> - **`presentSubscriptions` SUPPRIMÉ** sur iOS **et** Android + surface JS (≠ no-op). +> - **`fmt` est débloqué** par le `post_install` `FMT_USE_CONSTEVAL=0` du Podfile +> (hérité de la migration RN 0.86) — le doute n°2 est résolu. +> Vérifié : `yarn typecheck`/`lint` OK, Jest 136/136, et +> `react-native run-ios --device "iPhone KH"` = **built + installed + launched** +> sur device physique (Xcode 26.5, RN 0.86). Runtime des paywalls non encore exercé. + +--- + +## 1. Contexte et principe + +Le SDK React Native Purchasely est un **bridge JS/TS ↔ natif** (`NativeModules` / +`NativeEventEmitter`) vers les SDK natifs iOS (`Purchasely`) et Android +(`io.purchasely:core`). Cette migration **adapte le bridge aux SDK natifs 6.0**. + +Principe directeur (identique à la migration Flutter, validé avec le demandeur) : + +- **Pas de nommage « v6 » dans l'API publique TS.** Les nouvelles méthodes + **remplacent** l'existant. Quand il n'y a pas de nouvelle méthode native (ex. + `setUserAttribute*`), on **laisse en l'état**. +- Trois zones sont des breaking changes : **démarrage du SDK** (`builder`), + **affichage / preload / fermeture d'une présentation** (`PresentationBuilder` + → `PresentationRequest`), et **l'action interceptor** (`interceptAction` par + action retournant `'success' | 'failed' | 'notHandled'`). Le reste de la + surface `Purchasely.*` reste source-compatible. + +**L'essentiel de la couche TS, du bridge Android et du bridge iOS existait déjà** +sur la branche au début de la session (façade `builder`/`PresentationBuilder`/ +`interceptAction`, `PresentationOutcome` à 5 champs, vue inline `PLYPresentationView`, +bridges natifs réécrits, tests d'intégration, `MIGRATION-v6.md`, exemple). Le +travail de cette session a consisté à : **vérifier la compilation/le runtime +contre les vrais SDK natifs v6**, **ajouter le callback à `synchronize`**, +**corriger un test natif Android cassé**, **aligner les versions natives sur +l'artefact publié `6.0.0-rc.1`**, et **valider sur simulateur/émulateur**. + +--- + +## 2. Changements effectués cette session + +### 2.1 `synchronize()` — ajout du callback (TS + Android ; iOS déjà câblé) + +Les SDK natifs 6.0 exposent désormais des callbacks succès/erreur sur +`synchronize()`. Le bridge en profite : + +- **TS** (`packages/purchasely/src/index.ts`) : `synchronize()` passe de + `(): void` à **`(): Promise`**. La promesse résout à la fin de la + synchronisation et rejette en cas d'échec. **Source-compatible** : les appels + fire-and-forget existants (sans `await`) continuent de marcher. +- **Android** (`PurchaselyModule.kt`) : `synchronize(promise: Promise)` appelle + `Purchasely.synchronize(onSuccess = { promise.resolve(true) }, onError = { e -> … })`. + Signature native confirmée dans la source : + `fun synchronize(onSuccess: PLYPurchaseResultHandler? = null, onError: PLYErrorHandler? = null)` + (`@JvmOverloads`, chemin sans store → `onError(PLYError.NoStoreConfigured)`). + Avant : `fun synchronize() { Purchasely.synchronize() }` (fire-and-forget). +- **iOS** (`PurchaselyRN.m`) : **déjà câblé** sur la branche — + `RCT_EXPORT_METHOD(synchronize:reject:)` appelle déjà + `[Purchasely synchronizeWithSuccess:^{ resolve(@YES); } failure:^(NSError*e){ … }]`. + Aucune modification iOS nécessaire. + +### 2.2 Correction d'un test natif Android cassé (pré-existant) + +`PurchaselyModuleTest.kt` ne compilait plus contre `io.purchasely:core:6.0.0-rc.1` : +`Unresolved reference 'PLYPresentationType'` (l'enum a migré du package +`io.purchasely.ext` vers `io.purchasely.ext.presentation` en v6). Ajout de +`import io.purchasely.ext.presentation.PLYPresentationType` (le module +`PurchaselyModule.kt` l'importait déjà correctement). Débloque toute la suite de +tests natifs Android. + +### 2.3 Alignement des versions natives sur l'artefact **publié** `6.0.0-rc.1` + +Les pins étaient sur `6.0.0` (artefact non publié). Après vérification, la +version **réellement publiée** du milestone rc1 est `6.0.0-rc.1` (avec point) +sur **Maven Central** (Android) **et** sur le **trunk CocoaPods** (iOS) : + +| Réf | Avant | Après | Source de résolution | +|---|---|---|---| +| `packages/purchasely/android/build.gradle` (`core`) | `6.0.0` | `6.0.0-rc.1` | Maven Central | +| `packages/google/android/build.gradle` (`google-play`) | `6.0.0` | `6.0.0-rc.1` | Maven Central | +| `packages/android-player/android/build.gradle` (`player`) | `6.0.0` | `6.0.0-rc.1` | Maven Central | +| `packages/amazon/android/build.gradle` (`amazon`) | `6.0.0` | `6.0.0-rc.1` | Maven Central | +| `packages/huawei/android/build.gradle` (`huawei-services`) | `6.0.0` | `6.0.0-rc.1` | Maven Central | +| `packages/purchasely/react-native-purchasely.podspec` (`Purchasely`) | `6.0.0` | `6.0.0-rc.1` | CocoaPods trunk | + +> **iOS ET Android utilisent la MÊME chaîne `6.0.0-rc.1` (avec point).** C'est +> différent de la note de convention du rapport Flutter (qui annonçait Android +> `6.0.0-rc1` sans point) : pour React Native, les deux artefacts publiés +> portent `6.0.0-rc.1`. Le `6.0.0-rc1` (sans point) présent dans le `mavenLocal` +> de la machine était un build local périmé, **absent de Maven Central** (HTTP +> 404), à ne pas utiliser. + +> ⚠️ **Piège Gradle (leçon Flutter, vérifiée ici).** Gradle classe `6.0.0` +> (release) au-dessus de `6.0.0-rc.1` (pré-release) : une seule réf `io.purchasely:*` +> laissée en `6.0.0` (ou un `mavenLocal` contaminé) remonterait silencieusement +> le `core` → `NoSuchMethodError` au runtime. **Vérifié** : la résolution du +> `debugRuntimeClasspath` ne montre QUE `6.0.0-rc.1` (cf. §5). L'app exemple +> Android n'a aucune réf `io.purchasely:*` directe (héritage transitif des +> packages), donc aligner les 5 `build.gradle` suffit. + +> ✅ **Conséquence CI.** Les deux artefacts `6.0.0-rc.1` étant publiés (Maven +> Central + trunk CocoaPods), le CI natif ne dépend plus d'un `mavenLocal` / +> dev-pod local — ce qui devrait **débloquer les jobs `build-android` / +> `build-ios`** (voir §7, doute n°2). + +### 2.4 Tests TS ajoutés / mis à jour (`synchronize`) + +- `__mocks__/testUtils.ts` : mock `synchronize: jest.fn().mockResolvedValue(true)`. +- `__tests__/index.test.ts` : 2 tests ajoutés — résolution (`await … resolves true`) + et propagation d'erreur (`await … rejects`). + +### 2.5 Exemple + +- `example/src/Home.tsx` : `onPressSynchronize` illustre la nouvelle sémantique + (`await Purchasely.synchronize()` + `try/catch`). + +### 2.6 Documentation + +- `MIGRATION-v6.md` : section « Synchronize (now awaitable) » ajoutée + note + dans « What's UNCHANGED ». +- Ce rapport (`V6_MIGRATION_REPORT.md`). + +--- + +## 3. API TS v6 finale (référence pour `../Documentation` + `../purchasely-ai-skill`) + +### Initialisation (builder) + +```typescript +const configured = await Purchasely.builder('YOUR_API_KEY') + .appUserId('user_id') // optionnel + .runningMode('full') // 'observer' (défaut) | 'full' + .logLevel('error') // 'debug' | 'info' | 'warn' | 'error' + .allowDeeplink(true) // remplace readyToOpenDeeplink(true) + .allowCampaigns(true) // optionnel + .stores(['google']) // Android : 'google' | 'huawei' | 'amazon' + .storekitVersion('storeKit2') // iOS : 'storeKit1' | 'storeKit2' + .start() +``` + +> **Mode par défaut = `observer`** en v6. Passer `.runningMode('full')` si +> Purchasely doit gérer/valider les achats. + +### Affichage d'une présentation + +```typescript +const outcome = await Purchasely.presentation + .placement('ONBOARDING') // ou .screen('SCREEN_ID') / .default() + .contentId('content_id') // optionnel + .build() + .display({ type: 'fullScreen' }) + +// PresentationOutcome (5 champs) : +// presentation, purchaseResult, plan, closeReason, error +``` + +Cycle de vie : `const req = Purchasely.presentation.placement(id).build()` → +`req.preload()` → `req.display()` / `req.close()` / `req.back()`. + +### Action interceptor + +```typescript +Purchasely.interceptAction('purchase', async (info, payload) => { + if (payload?.kind === 'purchase') { /* … */ return 'success' } + return 'notHandled' // 'success' | 'failed' | 'notHandled' +}) +Purchasely.removeActionInterceptor('purchase') +Purchasely.removeAllActionInterceptors() +``` + +Kinds : `close, closeAll, login, navigate, purchase, restore, openPresentation, +openPlacement, promoCode, webCheckout`. + +### Inline (embarqué) + +```tsx + { /* … */ }} /> +``` + +### Synchronize (nouveau comportement) + +```typescript +try { + await Purchasely.synchronize() // résout à la fin ; rejette en cas d'échec +} catch (e) { /* PLYError.NoStoreConfigured, … */ } +``` + +### Inchangé (source-compatible) + +`purchaseWithPlanVendorId`, `signPromotionalOffer`, `restoreAllProducts`, +`silentRestoreAllProducts`, `userLogin`/`userLogout`, `isAnonymous`, +`getAnonymousUserId`, `allProducts`, `productWithIdentifier`, `planWithIdentifier`, +`isEligibleForIntroOffer`, `userSubscriptions`/`userSubscriptionsHistory`, +`setUserAttribute*` (+ increment/decrement/clear), `userAttributes`/`userAttribute`, +`addEventListener`/`addPurchasedListener`/`addUserAttribute*Listener`, +`setDynamicOffering`/`getDynamicOfferings`/`removeDynamicOffering`/`clearDynamicOfferings`, +`clientPresentationDisplayed`/`clientPresentationClosed`, `revokeDataProcessingConsent`, +`setLanguage`, `setThemeMode`, `setLogLevel`, `setDebugMode`, `isDeeplinkHandled`, +`userDidConsumeSubscriptionContent`. + +> `presentSubscriptions` a été **supprimé** (iOS + Android + surface JS) : la v6 +> native ne fournit plus d'UI native de liste d'abonnements. Construire son +> propre écran à partir de `userSubscriptions()`. + +--- + +## 4. Fichiers modifiés (cette session) + +- `packages/purchasely/src/index.ts` — `synchronize` → `Promise`. +- `packages/purchasely/android/.../PurchaselyModule.kt` — `synchronize(promise)` + + callbacks `onSuccess`/`onError`. +- `packages/purchasely/android/.../test/.../PurchaselyModuleTest.kt` — import + `PLYPresentationType` (package `presentation`). +- `packages/purchasely/android/build.gradle`, `packages/google/…`, + `packages/android-player/…`, `packages/amazon/…`, `packages/huawei/…` — pins + `6.0.0-rc.1`. +- `packages/purchasely/react-native-purchasely.podspec` — pin `Purchasely '6.0.0-rc.1'`. +- `packages/purchasely/src/__mocks__/testUtils.ts`, + `packages/purchasely/src/__tests__/index.test.ts` — tests `synchronize`. +- `example/src/Home.tsx` — exemple `synchronize` awaitable. +- `MIGRATION-v6.md`, `V6_MIGRATION_REPORT.md` — docs. + +**Non commité / local-machine :** +- `example/ios/Podfile.lock` (untracked) : régénéré, `Purchasely (6.0.0-rc.1)` + résolu depuis le trunk CocoaPods. + +**Éditions cross-repo (à NE PAS committer ici) :** +- `/Users/kevin/Purchasely/iOS/Purchasely.podspec` : version bumpée en `6.0.0-rc.1` + (édition antérieure). **N'est plus nécessaire** pour ce repo : le pod est + désormais résolu depuis le trunk CocoaPods, pas depuis un dev-pod local. + Peut être reverté. + +--- + +## 5. Vérifications exécutées (preuves) + +| Vérification | Commande | Résultat | +|---|---|---| +| TS typecheck | `yarn typecheck` | ✅ exit 0 | +| TS lint | `yarn lint` | ✅ exit 0 | +| TS tests (Jest) | `yarn test --maxWorkers=2` | ✅ **5 suites, 136 tests** (dont nouveaux `synchronize` + `presentation.integration`) | +| Build Android | `cd example/android && ./gradlew :app:assembleDebug` | ✅ BUILD SUCCESSFUL, `app-debug.apk` | +| Résolution Android | `./gradlew :app:dependencies --configuration debugRuntimeClasspath` | ✅ `io.purchasely:core|google-play:6.0.0-rc.1` **uniquement** (aucun `6.0.0`) | +| Tests natifs Android | `./gradlew :react-native-purchasely:testDebugUnitTest` | ✅ **41 tests, 0 échec** (PurchaselyModuleTest 32 + EnumOrdinalConsistencyTest 9) | +| Smoke Android (réel, Pixel_Tablet) | install APK + launch | ✅ `Init SDK (v.6.0.0-rc.1)`, `isSdkStarted=true`, `sdkVersion=6.0.0-rc.1`, `initialized successfully`, login/logout/anonymous | +| **Présentation v6 de bout en bout (Android)** | tap « Display Presentation » | ✅ `PRESENTATION_LOADED` → `PRESENTATION_VIEWED` → **paywall « PURCHASELY MUSIC » affiché plein écran** (rendu 2,6 s, 0 crash) | +| **`synchronize()` awaitable (Android, réel)** | tap « Synchronize » | ✅ `Synchronize purchases` → JS `Synchronize done` (la Promise résout) | +| pod install iOS | `cd example/ios && pod update Purchasely` | ✅ `Purchasely (6.0.0-rc.1)` intégré **depuis le trunk CocoaPods** | +| Build iOS (exemple) | `xcodebuild build -workspace example.xcworkspace -scheme example` | ⚠️ Collision d'assets **résolue** par le pod du trunk ; le build progresse jusqu'à la compilation et échoue **uniquement** sur `fmt` consteval dans `Pods/fmt/src/format.cc` (React Native core sous Xcode 26.5). 3 contournements tentés (2 CLI + 1 `post_install FMT_USE_CONSTEVAL=0`) sans effet : le `fmt` de RN 0.79 ignore le flag (clé sur `__cpp_consteval`). Fix réel = Xcode 16.x. Blocage **environnement RN/Xcode**, sans rapport avec Purchasely — cf. §7 doute n°2. | + +> Le backend est **réel** (clé API de l'exemple) : plans `PURCHASELY_PLUS_YEARLY/MONTHLY`, +> dynamic offerings, validation d'offre côté serveur, audience `play_store` — tout +> remonte du vrai backend Purchasely. + +--- + +## 6. Contrat de bridge (JS ↔ natif) + +- **NativeModules.Purchasely** : `start` (via builder), `preload`, `display`, + `closePresentation`/`hidePresentation`/`back`, `registerInterceptor`/ + `removeActionInterceptor`/`removeAllActionInterceptors`, `synchronize` + (Promise), + toute la surface conservée. +- **NativeEventEmitter** : `PURCHASELY_EVENTS`, `PURCHASE_LISTENER`, + `USER_ATTRIBUTE_SET_LISTENER`, `USER_ATTRIBUTE_REMOVED_LISTENER`, + + événements de présentation (`PURCHASELY_PRESENTATION_EVENTS`). + +--- + +## 7. Doutes / points à reviewer (À LIRE) + +1. **Convention de version (important, divergence avec Flutter).** Pour React + Native, l'artefact publié des deux côtés est **`6.0.0-rc.1`** (avec point) : + Maven Central (Android) ET trunk CocoaPods (iOS). Le rapport Flutter annonçait + Android `6.0.0-rc1` (sans point) ; ce n'est PAS le cas ici (le `6.0.0-rc1` + sans point n'existe qu'en `mavenLocal` local et renvoie 404 sur Maven Central). + **À confirmer** : la chaîne de version finale lors de la release (rc → final). + Mettre à jour les 6 pins en conséquence avant merge/release. + +2. **Build iOS local bloqué par React Native core (`fmt`/Xcode 26.5), PAS par + Purchasely.** L'intégration Purchasely iOS est prouvée propre : le pod + `6.0.0-rc.1` se résout depuis le trunk, s'intègre, et la collision d'assets + (`Assets.car`) — présente avec le dev-pod local de la branche `develop` — a + **disparu** avec le pod publié. Le seul échec restant est + `fmt::basic_format_string … is not a constant expression` dans `Pods/fmt` + (toolchain C++ de React Native 0.79 sous Xcode 26.5) — il toucherait + n'importe quelle app RN 0.79, indépendamment de Purchasely. 3 contournements + tentés sans succès : `-DFMT_CONSTEVAL=` et `-DFMT_USE_CONSTEVAL=0` en CLI (non + propagés à la cible `fmt`), puis un `post_install` posant `FMT_USE_CONSTEVAL=0` + par target — vérifié : le define **atteint** la compilation de `format.cc` + (`-DFMT_USE_CONSTEVAL=0` dans la commande clang) mais le `fmt` embarqué par RN + 0.79 **l'ignore** pour ce chemin (il clé `FMT_CONSTEVAL` sur `__cpp_consteval`). + ⇒ **pas de switch préprocesseur propre** ; il faut soit **builder avec un Xcode + compatible RN 0.79 (Xcode 16.x)**, soit upgrader React Native, soit patcher la + source `Pods/fmt` (fragile). Le `post_install` inefficace a été retiré. + Le CI (`macos-latest`, Xcode compatible) devrait builder normalement maintenant + que le pod est sur le trunk. **À reviewer** : la version d'Xcode du runner CI. + +3. **Le dev-pod iOS local n'est plus nécessaire.** La branche avait été testée + avec `pod 'Purchasely', :path => '/Users/kevin/Purchasely/iOS'`. Le pod étant + désormais sur le trunk, le Podfile de l'exemple est **inchangé** (pas de chemin + machine-spécifique committé) et l'édition de version du podspec iOS local peut + être revertée. + +4. **`synchronize()` rejette le chemin « no store ».** Sans store configuré, le + natif appelle `onError(PLYError.NoStoreConfigured)` → la Promise **rejette**. + Les appelants qui `await` doivent `catch` (l'exemple le fait). Les appels + fire-and-forget restent inoffensifs. + +5. **Pas de test unitaire natif `synchronize` ajouté côté Android.** Le chemin + no-store passe par `PLYLogger.w` → `PLYDiagnosticManager.addLog` (non gardé), + risqué en JUnit pur sans Robolectric ; et `Promise.reject` + null-safety + Kotlin/Mockito est fragile. Le bridge `synchronize` est couvert par : compile + contre `6.0.0-rc.1` (assembleDebug), tests JS resolve/reject, et test + fonctionnel réel (`Synchronize done`). À renforcer par un test natif si une + infra Robolectric est ajoutée (comme l'a fait Flutter). + +6. **Version du package RN → `6.0.0-rc.1`.** Alignée sur les natifs (iOS + + Android) : `purchaselyVersion` (`index.ts`), les 5 `package.json`, le test du + bridge (`index.test.ts`), `VERSIONS.md`, `sdk_public_doc.md`, `CLAUDE.md`. + Release prévue après review (les 5 packages npm doivent rester à la **même** + version). `CHANGELOG.md` garde son entête `6.0.0-beta.0` — à renommer au + moment de la release. + +7. **Validation fonctionnelle iOS.** Réalisée seulement sur Android faute de + build iOS local (cf. doute n°2). La couche JS/TS étant commune aux deux + plateformes et le bridge iOS `synchronize` étant déjà câblé (inchangé cette + session), le risque iOS spécifique est faible. À refaire sur un runner Xcode + compatible. + +--- + +## 8. Pour mettre à jour `../Documentation` et `../purchasely-ai-skill` + +- `purchasely-ai-skill/references/react-native/integration.md` : encore en **v5** + (`Purchasely.start({...})`, `fetchPresentation`/`presentPresentation`, + `setPaywallActionInterceptor` + `onProcessAction`). À remplacer par l'API v6 + (§3) : `builder`, `presentation`/`PresentationRequest`, `interceptAction`, + `PLYPresentationView`, `synchronize` awaitable. +- Créer `purchasely-ai-skill/references/react-native/migration-v6.md` (analogue + Android/iOS/Flutter) à partir de `MIGRATION-v6.md`. +- `purchasely-ai-skill/references/sdk-versions.md` : React Native passe de `5.7.3` + à la version v6 du package (cf. doute n°6), natifs `6.0.0-rc.1` (Android Maven + Central + iOS trunk CocoaPods). +- Docs publiques (`../Documentation`) : guide d'intégration RN + guide de + migration 5→6 RN, en miroir des guides Android/iOS/Flutter. diff --git a/VERSIONS.md b/VERSIONS.md index 31a940fc..81fe2f14 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -113,4 +113,5 @@ This file provides the underlying native SDK versions that the React Native SDK | 5.7.0 | 5.7.0 | 5.7.0 | | 5.7.1 | 5.7.1 | 5.7.1 | | 5.7.2 | 5.7.2 | 5.7.3 | +| 6.0.0-rc.1 | 6.0.0-rc.1 | 6.0.0-rc.1 | | 5.7.3 | 5.7.4 | 5.7.4 | diff --git a/docs/react-native-upgrade-best-practices.md b/docs/react-native-upgrade-best-practices.md new file mode 100644 index 00000000..dc413974 --- /dev/null +++ b/docs/react-native-upgrade-best-practices.md @@ -0,0 +1,599 @@ +# React Native Upgrade Best Practices + +> **Last Updated:** June 2026 +> **Purpose:** Reusable guide for future React Native version upgrades in SDK projects + +This document captures general best practices for upgrading React Native in SDK/library projects. For version-specific breaking changes, consult the official React Native release notes. + +--- + +## Table of Contents + +1. [Pre-Upgrade Checklist](#pre-upgrade-checklist) +2. [Understanding Version Coupling](#understanding-version-coupling) +3. [Phased Migration Strategy](#phased-migration-strategy) +4. [Essential Tools](#essential-tools) +5. [Native Bridge Considerations](#native-bridge-considerations) +6. [Testing Strategy](#testing-strategy) +7. [Common Breaking Changes by Category](#common-breaking-changes-by-category) +8. [Monorepo-Specific Guidance](#monorepo-specific-guidance) +9. [Version-Specific Quick Reference](#version-specific-quick-reference) + +--- + +## Pre-Upgrade Checklist + +Before starting any React Native upgrade: + +### 1. Audit Current State +- [ ] Document current RN version and all related tooling versions +- [ ] List all native modules and their New Architecture compatibility +- [ ] Check [React Native Directory](https://reactnative.directory/) for dependency compatibility +- [ ] Note any custom native code in iOS (`*.m`, `*.mm`, `*.swift`) and Android (`*.kt`, `*.java`) +- [ ] Run current test suite to establish baseline + +### 2. Review Target Version +- [ ] Read the official release blog post at `reactnative.dev/blog` +- [ ] Check minimum requirements (Node.js, Xcode, Gradle, Kotlin) +- [ ] Review breaking changes section +- [ ] Note deprecated APIs that affect your codebase + +### 3. Prepare Environment +- [ ] Update local development tools to meet requirements +- [ ] Create feature branch for migration +- [ ] Set up CI to test against both old and new versions (temporarily) +- [ ] Establish rollback plan + +### 4. Establish Metrics +- [ ] App startup time +- [ ] Module initialization time +- [ ] Memory usage during key operations +- [ ] Build times for both platforms + +--- + +## Understanding Version Coupling + +### For SDK/Library Projects + +**peerDependencies Pattern (Recommended):** +```json +{ + "peerDependencies": { + "react": "*", + "react-native": "*" + }, + "devDependencies": { + "react": "19.x.x", + "react-native": "0.8x.x" + } +} +``` + +**Why this pattern:** +- SDK consumers can use any RN version +- `devDependencies` only affects development/testing +- Flexibility without forcing consumer upgrades + +**Native Bridge Considerations:** +- iOS: Use `s.dependency "React-Core"` without version constraint +- Android: Use `api 'com.facebook.react:react-native:+'` for dynamic versioning + +### Files That Pin Versions + +| File Type | Purpose | Update Strategy | +|-----------|---------|-----------------| +| `package.json` devDeps | Development environment | Update for team consistency | +| Example app `package.json` | Reference implementation | Update to match target version | +| Test project `package.json` | CI testing | Update or maintain multiple versions | +| `.nvmrc` | Node.js version | Update to meet minimum requirements | +| `gradle-wrapper.properties` | Gradle version | Update per RN requirements | +| `Podfile` | iOS dependencies | Usually auto-detected | + +--- + +## Phased Migration Strategy + +### Recommended Approach: Incremental Upgrades + +``` +Current Version + ↓ +Minor Version Steps (if large gap) + ↓ +Second-to-Last Major Version + ↓ +Enable New Features/Architecture + ↓ +Latest Version + ↓ +Optimize/Clean Up +``` + +### Phase Template + +**Phase 1: Assessment (1-2 days)** +- Review breaking changes documentation +- Audit dependencies for compatibility +- Create migration branch +- Identify high-risk areas + +**Phase 2: Incremental Upgrade (1-3 days per version)** +- Use Upgrade Helper for file-by-file changes +- Update dependencies one version at a time +- Fix build errors before runtime testing +- Document all changes made + +**Phase 3: Feature Migration (1-5 days)** +- Enable new architecture features +- Test with feature flags +- Address deprecation warnings +- Migrate deprecated APIs + +**Phase 4: Validation (2-3 days)** +- Full test suite execution +- Performance comparison +- Platform-specific testing +- Edge case verification + +**Phase 5: Rollout** +- Update documentation +- Prepare changelog +- Consider phased release + +--- + +## Essential Tools + +### React Native Upgrade Helper +**URL:** https://react-native-community.github.io/upgrade-helper/ + +**Usage:** +1. Select your current version +2. Select target version +3. Review file-by-file diff +4. Apply changes systematically +5. Mark files as done + +**Best Practices:** +- Don't skip versions in the helper for large jumps +- Pay attention to inline comments (insights about specific changes) +- Keep the helper open during entire migration + +### React Native CLI Upgrade +```bash +# Automatic upgrade with git-based merging +npx react-native upgrade + +# Upgrade to specific version +npx react-native upgrade 0.83.0 +``` + +**When to use CLI vs Manual:** +- CLI: Clean projects, small version jumps +- Manual (Upgrade Helper): Complex projects, many customizations + +### React Native Directory +**URL:** https://reactnative.directory/ + +**Filter by:** +- "New Architecture" support +- Platform (iOS/Android) +- Maintenance status + +### Additional Tools +| Tool | Purpose | +|------|---------| +| `npx expo-codemod` | Automatic code transformations | +| Flipper | Debugging during migration | +| React DevTools | Component render analysis | + +--- + +## Native Bridge Considerations + +### iOS Native Modules + +**Standard Module Registration:** +```objc +// Pre-New Architecture (still works via interop) +RCT_EXPORT_MODULE() +RCT_EXPORT_METHOD(methodName:(NSString *)param + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +``` + +**TurboModules (New Architecture):** +```typescript +// NativeYourModule.ts (Codegen spec) +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface Spec extends TurboModule { + methodName(param: string): Promise; +} + +export default TurboModuleRegistry.getEnforcing('YourModule'); +``` + +**Migration Strategy:** +1. Legacy modules work via interop layer in New Architecture +2. Create codegen spec for TurboModules when ready +3. Both can coexist during transition + +### Android Native Modules + +**Standard Module:** +```kotlin +class YourModule(reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext) { + + override fun getName() = "YourModule" + + @ReactMethod + fun methodName(param: String, promise: Promise) { + // Implementation + } +} +``` + +**Package Registration:** +```kotlin +class YourPackage : ReactPackage { + override fun createNativeModules(context: ReactApplicationContext) = + listOf(YourModule(context)) + + override fun createViewManagers(context: ReactApplicationContext) = + emptyList>() +} +``` + +**TurboModule Enhancement:** +```kotlin +// Add ReactModuleInfo for TurboModule support +override fun getReactModuleInfoProvider() = ReactModuleInfoProvider { + mapOf( + YourModule.NAME to ReactModuleInfo( + name = YourModule.NAME, + className = YourModule.NAME, + canOverrideExistingModule = false, + needsEagerInit = false, + isCxxModule = false, + isTurboModule = true + ) + ) +} +``` + +### Native View Components + +**Legacy Pattern (works via Fabric interop):** +```typescript +import { requireNativeComponent } from 'react-native'; +const NativeView = requireNativeComponent('YourViewName'); +``` + +**Fabric Pattern:** +```typescript +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +const NativeView = codegenNativeComponent('YourViewName'); +``` + +--- + +## Testing Strategy + +### Test Pyramid for RN Upgrades + +``` + ┌─────────────────┐ + │ E2E Tests │ ← Detox, Maestro + │ (Selective) │ + ├─────────────────┤ + │ Integration │ ← Example App Manual Testing + │ Tests │ + ├─────────────────┤ + │ Unit Tests │ ← Jest (most important) + └─────────────────┘ +``` + +### Test Checklist by Phase + +**After Build Success:** +- [ ] All Jest unit tests pass +- [ ] TypeScript compiles without errors +- [ ] No new lint warnings + +**After App Runs:** +- [ ] App launches on iOS simulator +- [ ] App launches on Android emulator +- [ ] Hot reload works +- [ ] Native modules load correctly + +**Functional Testing:** +- [ ] All critical user flows work +- [ ] Native bridge methods return expected values +- [ ] Event listeners fire correctly +- [ ] Async operations complete + +**Platform-Specific:** +- [ ] iOS: Test on physical device +- [ ] iOS: Test production build +- [ ] Android: Test release APK +- [ ] Android: Test with ProGuard/R8 + +### Mocking Strategy for Tests + +**Legacy NativeModules:** +```javascript +jest.mock('react-native', () => ({ + ...jest.requireActual('react-native'), + NativeModules: { + YourModule: { + methodName: jest.fn().mockResolvedValue(true), + }, + }, +})); +``` + +**TurboModules (if migrated):** +```javascript +jest.mock('./NativeYourModule', () => ({ + default: { + methodName: jest.fn().mockResolvedValue(true), + }, +})); +``` + +--- + +## Common Breaking Changes by Category + +### Build System Changes + +| Category | What to Check | +|----------|---------------| +| Gradle | Version in `gradle-wrapper.properties` | +| Kotlin | Version in root `build.gradle` | +| AGP | Android Gradle Plugin version | +| Xcode | Minimum version in release notes | +| CocoaPods | May need `pod install --repo-update` | +| Node.js | Minimum version in `.nvmrc` | + +### JavaScript/TypeScript Changes + +| Category | Common Issues | +|----------|---------------| +| Deep imports | `react-native/Libraries/...` → root imports | +| Event emitters | API changes, thread safety | +| Component deprecations | SafeAreaView, StatusBar methods | +| Hooks | ESLint rule updates | + +### Native Code Changes + +| Platform | Common Issues | +|----------|---------------| +| iOS | Header includes, macro changes, Swift version | +| Android | Class visibility changes, Kotlin nullability, package moves | +| Both | Module registration, view manager patterns | + +### Architecture Changes + +| Era | Key Characteristics | +|-----|---------------------| +| Pre-0.68 | Bridge-only architecture | +| 0.68-0.73 | New Architecture optional (opt-in) | +| 0.74+ | New Architecture default (opt-out) | + +--- + +## Monorepo-Specific Guidance + +### Package Coordination + +**Upgrade Order:** +1. Core SDK package first +2. Platform-specific packages (in parallel) +3. Example apps +4. Test projects + +**Version Consistency:** +```json +// Root package.json +{ + "resolutions": { + "react-native": "0.83.x" + } +} +``` + +### Metro Configuration + +**Monorepo Metro Config:** +```javascript +// metro.config.js +const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); +const path = require('path'); + +const config = { + watchFolders: [path.resolve(__dirname, '../../packages')], + resolver: { + nodeModulesPaths: [ + path.resolve(__dirname, 'node_modules'), + path.resolve(__dirname, '../../node_modules'), + ], + }, +}; + +module.exports = mergeConfig(getDefaultConfig(__dirname), config); +``` + +### Workspace Tools + +| Tool | Purpose | +|------|---------| +| Yarn Workspaces | Package management | +| Turborepo | Build orchestration, caching | +| `@rnx-kit/metro-config` | Metro in monorepos | +| `@rnx-kit/metro-resolver-symlinks` | Symlink resolution | + +--- + +## Version-Specific Quick Reference + +### Checking Current Requirements + +```bash +# Check RN version +npx react-native --version + +# Check Node version +node --version + +# Check installed RN info +npx react-native info +``` + +### Release Cadence + +React Native releases approximately every 60 days. For enterprise stability: +- Wait 2-4 weeks after release for patch versions +- Target versions that remain "latest" for at least 3 months +- Monitor [React Native releases](https://github.com/facebook/react-native/releases) + +### Breaking Changes Reference URLs + +| Version | Release Notes URL | +|---------|-------------------| +| Latest | https://reactnative.dev/blog | +| Specific | https://reactnative.dev/blog/YYYY/MM/DD/react-native-X.XX | +| Changelog | https://github.com/facebook/react-native/blob/main/CHANGELOG.md | + +### Toolchain Reference by Upgraded Version + +This SDK's verified toolchain pins per React Native version. Use as a known-good baseline. + +| RN Version | React | Node (`.nvmrc`) | TypeScript | `@types/react` | Gradle | compile/target SDK | iOS min | Kotlin | `@react-native-community/cli` | +|------------|-------|-----------------|------------|----------------|--------|--------------------|---------|--------|-------------------------------| +| 0.79.2 | 19.0.0 | v20 | ^5.2.2 | ^18.2.x | 8.12 | 35 | 15.1 | 15.x | 15.0.1 | +| 0.83.x | 19.x | v20.19.4 | ^5.x | ^19.x | 8.14 | 35 | 15.1 | 2.1.21 | 18.x | +| 0.86.0 | 19.2.3 | v22 | ^5.8.3 | ^19.2.0 | 9.3.1 | 36 | 15.1 | 2.1.21 | 20.1.0 | + +> Verified build: `:app:assembleDebug` green with NDK 27.1.12297006, `react-native-screens@^4.16` (New-Arch C++), and the `rnc-cli` permission `postinstall` in place. + +**RN 0.86.0 notes:** +- Node minimum raised to **22.11** (bump `.nvmrc` to `v22` and every `engines.node`). +- `@react-native/*` packages (babel-preset, eslint-config, metro-config, typescript-config) and the example app all move to `0.86.0`; `@react-native-community/cli*` to `20.1.0`. +- Metro: `@react-native/metro-config@0.86.0` pulls metro `^0.84.x`, so bump `metro-cache` / `metro-config` devDeps to `^0.84.0` to avoid a duplicated metro tree. +- iOS `IPHONEOS_DEPLOYMENT_TARGET` stays **15.1** (`min_ios_version_supported` in RN 0.86 is still 15.1 — no pbxproj change needed). +- Android: `buildToolsVersion`/`compileSdkVersion`/`targetSdkVersion` → **36**, `minSdkVersion` stays 24, `ndkVersion` stays 27.1.12297006. +- Native entry points (`MainApplication.kt`, `MainActivity.kt`, `AppDelegate.swift`) already use the New Architecture factory (`DefaultReactNativeHost` / `getDefaultReactHost` / `RCTReactNativeFactory`) since 0.79 — no changes required. +- The obsolete `@types/react-native` resolution is removed (RN ships its own bundled types). +- **Jest preset moved out of `react-native`**: the `react-native/jest-preset` shim was removed. Add the `@react-native/jest-preset@0.86.0` devDep to every package/app that runs Jest and change the Jest `preset` from `react-native` to `@react-native/jest-preset` (in `package.json` `jest.preset` and any `jest.config.js`). +- **Hermes Flow syntax in RN sources**: RN 0.86's own JS (e.g. `@react-native/js-polyfills/error-guard.js`) uses Hermes-flavoured Flow (`>` bounds) that `@babel/preset-flow` cannot parse. `babel-jest` therefore needs `@react-native/babel-preset` (which bundles `babel-plugin-syntax-hermes-parser`). Point each package's `babel.config.js` at `module:@react-native/babel-preset` (add it as a devDep) instead of `react-native-builder-bob/babel-preset`. This only affects Jest — `bob build` uses its own internal preset, so the published `lib/` output is unchanged. +- **`@react-native-community/cli@20.x` ships a non-executable `build/bin.js`** (tarball mode `0644`). npm fixes this from the `bin` field but Yarn's node-modules linker preserves `0644`, so Gradle autolinking (`autolinkLibrariesFromCommand()` in `settings.gradle`) fails with `npx ... exit value 126` (Permission denied). Fix with a root `postinstall` that `chmod +x` the CLI bin (see `scripts/fix-rnc-cli-permissions.js`). +- **New-Architecture C++ native deps must be bumped in lockstep**: RN 0.86 removed `facebook::react::ShadowNode::Shared` (now `std::shared_ptr`). `react-native-screens` < 4.16 fails to compile against the 0.86 prefab headers — bump to `^4.16` (4.25.x at time of writing). Audit every dependency with autolinked C++/codegen (`react-native-screens`, `react-native-safe-area-context`, …) when the New Arch is enabled. + +### Architecture Feature Flags + +**Android (`gradle.properties`):** +```properties +# Enable New Architecture +newArchEnabled=true + +# Enable Hermes (default in recent versions) +hermesEnabled=true +``` + +**iOS (Environment or Podfile):** +```bash +# Environment variable +RCT_NEW_ARCH_ENABLED=1 bundle exec pod install + +# Or in Podfile +ENV['RCT_NEW_ARCH_ENABLED'] = '1' +``` + +--- + +## Quick Decision Guide + +### Should I Upgrade? + +**Upgrade Now If:** +- Security vulnerabilities in current version +- Blocking bug fixed in newer version +- New feature needed for business requirement +- Current version losing support + +**Delay Upgrade If:** +- Critical dependencies not compatible +- Major release deadline approaching +- Insufficient testing resources +- New version < 4 weeks old + +### How Many Versions to Jump? + +| Gap | Recommendation | +|-----|----------------| +| 1-2 minor versions | Direct upgrade | +| 3-4 minor versions | Consider intermediate step | +| Major version change | Always use intermediate steps | +| Architecture change (e.g., New Arch) | Dedicated migration phase | + +--- + +## Troubleshooting Common Issues + +### Build Failures + +```bash +# Clean everything and retry +yarn clean # or npm run clean +cd ios && pod deintegrate && pod install +cd android && ./gradlew clean +``` + +### Metro Issues + +```bash +# Clear Metro cache +npx react-native start --reset-cache + +# Or with Expo +npx expo start --clear +``` + +### CocoaPods Issues + +```bash +cd ios +pod deintegrate +pod cache clean --all +pod install --repo-update +``` + +### Android Gradle Issues + +```bash +cd android +./gradlew clean +./gradlew --stop +rm -rf ~/.gradle/caches +./gradlew build +``` + +--- + +## Maintenance + +**Update this document when:** +- Completing a React Native upgrade +- Discovering new best practices +- React Native release cadence changes +- New Architecture becomes the only option (post-0.82) + +**Review annually for:** +- Outdated tool recommendations +- Changed URLs +- Deprecated practices diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index e1a7ce8c..6ba9950c 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -23,6 +23,12 @@ react { // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. // debuggableVariants = ["liteDebug", "prodDebug"] + // In CI (E2E builds), remove 'debug' from debuggable variants so the JS bundle + // is embedded in the APK (no Metro bundler needed on the emulator). + if (project.hasProperty('e2eBuild')) { + debuggableVariants = [] + } + /* Bundling */ // A list containing the node command and its flags. Default is just 'node'. // nodeExecutableAndArgs = ["node"] diff --git a/example/android/app/src/main/java/com/purchasely/MainActivity.kt b/example/android/app/src/main/java/com/purchasely/MainActivity.kt index 0ddd9029..049a57dd 100644 --- a/example/android/app/src/main/java/com/purchasely/MainActivity.kt +++ b/example/android/app/src/main/java/com/purchasely/MainActivity.kt @@ -1,5 +1,6 @@ package com.purchasely +import android.os.Bundle import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled @@ -7,16 +8,36 @@ import com.facebook.react.defaults.DefaultReactActivityDelegate class MainActivity : ReactActivity() { - /** - * Returns the name of the main component registered from JavaScript. This is used to schedule - * rendering of the component. - */ + // Resolved once in onCreate() so the E2E flag is stable before super.onCreate() runs. + private var isE2eMode = false + + override fun onCreate(savedInstanceState: Bundle?) { + isE2eMode = intent?.getStringExtra("E2E_MODE") == "true" + super.onCreate(savedInstanceState) + } + override fun getMainComponentName(): String = "example" - /** - * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] - * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] - */ override fun createReactActivityDelegate(): ReactActivityDelegate = - DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) + object : DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) { + // Pass e2eMode as an initial prop so the JS root component can switch + // to the E2ETestRunner without needing a separate component name. + override fun getLaunchOptions(): Bundle = + Bundle().apply { + putBoolean("e2eMode", isE2eMode) + if (isE2eMode) { + putString("phase", intent?.getStringExtra("E2E_PHASE") ?: "all") + } + } + + // In E2E mode: do NOT notify React Native when MainActivity loses focus + // to a child Activity (e.g. PLYFlowActivity). Without this override, + // onHostPause() suspends the Hermes timer queue, freezing all JS awaits + // (sleep, waitFor, Promise.race) for the duration of the paywall display. + override fun onPause() { + if (!isE2eMode) { + super.onPause() + } + } + } } diff --git a/example/android/build.gradle b/example/android/build.gradle index 05a9c8d9..13f75b1e 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,9 +1,9 @@ buildscript { ext { - buildToolsVersion = "35.0.0" + buildToolsVersion = "36.0.0" minSdkVersion = 24 - compileSdkVersion = 35 - targetSdkVersion = 35 + compileSdkVersion = 36 + targetSdkVersion = 36 ndkVersion = "27.1.12297006" kotlinVersion = "2.1.21" } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index e0fd0202..37f78a6a 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/example/index.js b/example/index.js index 69303b34..6c667a06 100644 --- a/example/index.js +++ b/example/index.js @@ -2,8 +2,19 @@ * @format */ +import React from 'react'; import {AppRegistry} from 'react-native'; import App from './src/App'; import {name as appName} from './app.json'; +import E2ETestRunner from './src/E2ETestRunner'; -AppRegistry.registerComponent(appName, () => App); +// When launched with E2E_MODE intent extra, MainActivity passes e2eMode=true +// as an initial prop, which routes to the test runner component. +const RootComponent = (props) => { + if (props.e2eMode) { + return React.createElement(E2ETestRunner, props); + } + return React.createElement(App, props); +}; + +AppRegistry.registerComponent(appName, () => RootComponent); diff --git a/example/ios/Podfile b/example/ios/Podfile index 899b463d..e2ecae56 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -31,5 +31,20 @@ target 'example' do :mac_catalyst_enabled => false, # :ccache_enabled => true ) + + # TODO(fmt-consteval): remove once React Native ships a fmt version that + # compiles under Xcode 26.x clang. Tracking: upstream fmt + RN bundled fmt. + # Xcode 26 / recent Clang reject fmt's consteval-based compile-time + # format-string checks ("call to consteval function ... is not a constant + # expression"). Disable fmt's consteval path on every target so the bundled + # fmt pod and its consumers (Folly, React-*) build under Xcode 26.x. + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + defs = config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] || ['$(inherited)'] + defs = [defs] unless defs.is_a?(Array) + defs << 'FMT_USE_CONSTEVAL=0' unless defs.include?('FMT_USE_CONSTEVAL=0') + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = defs + end + end end end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 00000000..238f5f72 --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,2281 @@ +PODS: + - FBLazyVector (0.86.0) + - hermes-engine (250829098.0.14): + - hermes-engine/Pre-built (= 250829098.0.14) + - hermes-engine/Pre-built (250829098.0.14) + - Purchasely (6.0.0-rc.2) + - RCTDeprecation (0.86.0) + - RCTRequired (0.86.0) + - RCTSwiftUI (0.86.0) + - RCTSwiftUIWrapper (0.86.0): + - RCTSwiftUI + - RCTTypeSafety (0.86.0): + - FBLazyVector (= 0.86.0) + - RCTRequired (= 0.86.0) + - React-Core (= 0.86.0) + - React (0.86.0): + - React-Core (= 0.86.0) + - React-Core/DevSupport (= 0.86.0) + - React-Core/RCTWebSocket (= 0.86.0) + - React-RCTActionSheet (= 0.86.0) + - React-RCTAnimation (= 0.86.0) + - React-RCTBlob (= 0.86.0) + - React-RCTImage (= 0.86.0) + - React-RCTLinking (= 0.86.0) + - React-RCTNetwork (= 0.86.0) + - React-RCTSettings (= 0.86.0) + - React-RCTText (= 0.86.0) + - React-RCTVibration (= 0.86.0) + - React-callinvoker (0.86.0) + - React-Core (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default (= 0.86.0) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core-prebuilt (0.86.0): + - ReactNativeDependencies + - React-Core/CoreModulesHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/Default (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/DevSupport (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default (= 0.86.0) + - React-Core/RCTWebSocket (= 0.86.0) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTActionSheetHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTAnimationHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTBlobHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTImageHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTLinkingHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTNetworkHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTSettingsHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTTextHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTVibrationHeaders (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-Core/RCTWebSocket (0.86.0): + - hermes-engine + - RCTDeprecation + - React-Core-prebuilt + - React-Core/Default (= 0.86.0) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsinspectorcdp + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-CoreModules (0.86.0): + - RCTTypeSafety (= 0.86.0) + - React-Core-prebuilt + - React-Core/CoreModulesHeaders (= 0.86.0) + - React-debug + - React-featureflags + - React-jsi (= 0.86.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-NativeModulesApple + - React-RCTBlob + - React-RCTFBReactNativeSpec + - React-RCTImage (= 0.86.0) + - React-runtimeexecutor + - React-utils + - ReactCommon + - ReactNativeDependencies + - React-cxxreact (0.86.0): + - hermes-engine + - React-callinvoker (= 0.86.0) + - React-Core-prebuilt + - React-debug (= 0.86.0) + - React-jsi (= 0.86.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-logger (= 0.86.0) + - React-perflogger (= 0.86.0) + - React-runtimeexecutor + - React-timing (= 0.86.0) + - React-utils + - ReactNativeDependencies + - React-debug (0.86.0): + - React-debug/redbox (= 0.86.0) + - React-debug/redbox (0.86.0) + - React-defaultsnativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-domnativemodule + - React-Fabric/animated + - React-featureflags + - React-featureflagsnativemodule + - React-idlecallbacksnativemodule + - React-intersectionobservernativemodule + - React-jsi + - React-jsiexecutor + - React-microtasksnativemodule + - React-mutationobservernativemodule + - React-RCTFBReactNativeSpec + - React-viewtransitionnativemodule + - React-webperformancenativemodule + - ReactNativeDependencies + - Yoga + - React-domnativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-Fabric + - React-Fabric/bridging + - React-FabricComponents + - React-graphics + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-Fabric (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/animated (= 0.86.0) + - React-Fabric/animationbackend (= 0.86.0) + - React-Fabric/animations (= 0.86.0) + - React-Fabric/attributedstring (= 0.86.0) + - React-Fabric/bridging (= 0.86.0) + - React-Fabric/componentregistry (= 0.86.0) + - React-Fabric/componentregistrynative (= 0.86.0) + - React-Fabric/components (= 0.86.0) + - React-Fabric/consistency (= 0.86.0) + - React-Fabric/core (= 0.86.0) + - React-Fabric/dom (= 0.86.0) + - React-Fabric/imagemanager (= 0.86.0) + - React-Fabric/leakchecker (= 0.86.0) + - React-Fabric/mounting (= 0.86.0) + - React-Fabric/observers (= 0.86.0) + - React-Fabric/scheduler (= 0.86.0) + - React-Fabric/telemetry (= 0.86.0) + - React-Fabric/uimanager (= 0.86.0) + - React-Fabric/viewtransition (= 0.86.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/animated (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/animationbackend + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/animationbackend (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/animations (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/attributedstring (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/bridging (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/componentregistry (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/componentregistrynative (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/components/legacyviewmanagerinterop (= 0.86.0) + - React-Fabric/components/root (= 0.86.0) + - React-Fabric/components/scrollview (= 0.86.0) + - React-Fabric/components/view (= 0.86.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/legacyviewmanagerinterop (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/root (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/scrollview (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/components/view (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-renderercss + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-Fabric/consistency (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/core (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/dom (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/imagemanager (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/leakchecker (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/mounting (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-jsinspectortracing + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/observers (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/observers/events (= 0.86.0) + - React-Fabric/observers/intersection (= 0.86.0) + - React-Fabric/observers/mutation (= 0.86.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/observers/events (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/observers/intersection (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/observers/mutation (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/scheduler (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/animationbackend + - React-Fabric/observers/events + - React-Fabric/viewtransition + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-performancecdpmetrics + - React-performancetimeline + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/telemetry (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/uimanager (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric/uimanager/consistency (= 0.86.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/uimanager/consistency (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-Fabric/viewtransition (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-FabricComponents (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-FabricComponents/components (= 0.86.0) + - React-FabricComponents/textlayoutmanager (= 0.86.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-FabricComponents/components/inputaccessory (= 0.86.0) + - React-FabricComponents/components/iostextinput (= 0.86.0) + - React-FabricComponents/components/modal (= 0.86.0) + - React-FabricComponents/components/rncore (= 0.86.0) + - React-FabricComponents/components/safeareaview (= 0.86.0) + - React-FabricComponents/components/scrollview (= 0.86.0) + - React-FabricComponents/components/switch (= 0.86.0) + - React-FabricComponents/components/text (= 0.86.0) + - React-FabricComponents/components/textinput (= 0.86.0) + - React-FabricComponents/components/unimplementedview (= 0.86.0) + - React-FabricComponents/components/virtualview (= 0.86.0) + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/inputaccessory (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/iostextinput (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/modal (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/rncore (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/safeareaview (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/scrollview (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/switch (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/text (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/textinput (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/unimplementedview (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/components/virtualview (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricComponents/textlayoutmanager (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-RCTFBReactNativeSpec + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-FabricImage (0.86.0): + - hermes-engine + - RCTRequired (= 0.86.0) + - RCTTypeSafety (= 0.86.0) + - React-Core-prebuilt + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-jsiexecutor (= 0.86.0) + - React-logger + - React-rendererdebug + - React-utils + - ReactCommon + - ReactNativeDependencies + - Yoga + - React-featureflags (0.86.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-featureflagsnativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-graphics (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-jsi + - React-jsiexecutor + - React-rendererdebug + - React-utils + - ReactNativeDependencies + - React-hermes (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact (= 0.86.0) + - React-jsi + - React-jsiexecutor (= 0.86.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-oscompat + - React-perflogger (= 0.86.0) + - React-runtimeexecutor + - ReactNativeDependencies + - React-idlecallbacksnativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - React-runtimescheduler + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-ImageManager (0.86.0): + - React-Core-prebuilt + - React-Core/Default + - React-debug + - React-Fabric + - React-graphics + - React-rendererdebug + - React-utils + - ReactNativeDependencies + - React-intersectionobservernativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-Fabric + - React-Fabric/bridging + - React-graphics + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - React-runtimescheduler + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-jserrorhandler (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-jsi + - ReactCommon/turbomodule/bridging + - ReactNativeDependencies + - React-jsi (0.86.0): + - hermes-engine + - React-Core-prebuilt + - ReactNativeDependencies + - React-jsiexecutor (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-jserrorhandler + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-perflogger + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-jsinspector (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-jsi + - React-jsinspectorcdp + - React-jsinspectornetwork + - React-jsinspectortracing + - React-oscompat + - React-perflogger (= 0.86.0) + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-jsinspectorcdp (0.86.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-jsinspectornetwork (0.86.0): + - React-Core-prebuilt + - React-jsinspectorcdp + - ReactNativeDependencies + - React-jsinspectortracing (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-jsinspectornetwork + - React-oscompat + - React-timing + - React-utils + - ReactNativeDependencies + - React-jsitooling (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact (= 0.86.0) + - React-debug + - React-jsi (= 0.86.0) + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-jsitracing (0.86.0): + - React-jsi + - React-logger (0.86.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-Mapbuffer (0.86.0): + - React-Core-prebuilt + - React-debug + - ReactNativeDependencies + - React-microtasksnativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-mutationobservernativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-Fabric + - React-Fabric/bridging + - React-Fabric/observers/mutation + - React-featureflags + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - react-native-purchasely (6.0.0-rc.1): + - Purchasely (= 6.0.0-rc.2) + - React-Core + - react-native-safe-area-context (5.5.2): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - react-native-safe-area-context/common (= 5.5.2) + - react-native-safe-area-context/fabric (= 5.5.2) + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - react-native-safe-area-context/common (5.5.2): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - react-native-safe-area-context/fabric (5.5.2): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - react-native-safe-area-context/common + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-NativeModulesApple (0.86.0): + - hermes-engine + - React-callinvoker + - React-Core + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-runtimeexecutor + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - React-networking (0.86.0): + - React-Core-prebuilt + - React-jsinspectornetwork + - React-jsinspectortracing + - React-performancetimeline + - React-timing + - ReactNativeDependencies + - React-oscompat (0.86.0) + - React-perflogger (0.86.0): + - React-Core-prebuilt + - ReactNativeDependencies + - React-performancecdpmetrics (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-jsi + - React-performancetimeline + - React-runtimeexecutor + - React-timing + - ReactNativeDependencies + - React-performancetimeline (0.86.0): + - React-Core-prebuilt + - React-featureflags + - React-jsinspector + - React-jsinspectortracing + - React-perflogger + - React-timing + - ReactNativeDependencies + - React-RCTActionSheet (0.86.0): + - React-Core/RCTActionSheetHeaders (= 0.86.0) + - React-RCTAnimation (0.86.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTAnimationHeaders + - React-debug + - React-featureflags + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-RCTAppDelegate (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-CoreModules + - React-debug + - React-defaultsnativemodule + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-jsitooling + - React-NativeModulesApple + - React-RCTFabric + - React-RCTFBReactNativeSpec + - React-RCTImage + - React-RCTNetwork + - React-RCTRuntime + - React-rendererdebug + - React-RuntimeApple + - React-RuntimeCore + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactCommon + - ReactNativeDependencies + - React-RCTBlob (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-Core/RCTBlobHeaders + - React-Core/RCTWebSocket + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - React-RCTNetwork + - ReactCommon + - ReactNativeDependencies + - React-RCTFabric (0.86.0): + - hermes-engine + - RCTSwiftUIWrapper + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-FabricComponents + - React-FabricImage + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-networking + - React-performancecdpmetrics + - React-performancetimeline + - React-RCTAnimation + - React-RCTFBReactNativeSpec + - React-RCTImage + - React-RCTText + - React-rendererconsistency + - React-renderercss + - React-rendererdebug + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - Yoga + - React-RCTFBReactNativeSpec (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec/components (= 0.86.0) + - ReactCommon + - ReactNativeDependencies + - React-RCTFBReactNativeSpec/components (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-jsi + - React-NativeModulesApple + - React-rendererdebug + - React-utils + - ReactCommon + - ReactNativeDependencies + - Yoga + - React-RCTImage (0.86.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTImageHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - React-RCTNetwork + - ReactCommon + - ReactNativeDependencies + - React-RCTLinking (0.86.0): + - React-Core/RCTLinkingHeaders (= 0.86.0) + - React-jsi (= 0.86.0) + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactCommon/turbomodule/core (= 0.86.0) + - React-RCTNetwork (0.86.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTNetworkHeaders + - React-debug + - React-featureflags + - React-jsi + - React-jsinspectorcdp + - React-jsinspectornetwork + - React-NativeModulesApple + - React-networking + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-RCTRuntime (0.86.0): + - hermes-engine + - React-Core + - React-Core-prebuilt + - React-debug + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-RuntimeApple + - React-RuntimeCore + - React-runtimeexecutor + - React-RuntimeHermes + - React-utils + - ReactNativeDependencies + - React-RCTSettings (0.86.0): + - RCTTypeSafety + - React-Core-prebuilt + - React-Core/RCTSettingsHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-RCTText (0.86.0): + - React-Core/RCTTextHeaders (= 0.86.0) + - Yoga + - React-RCTVibration (0.86.0): + - React-Core-prebuilt + - React-Core/RCTVibrationHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTFBReactNativeSpec + - ReactCommon + - ReactNativeDependencies + - React-rendererconsistency (0.86.0) + - React-renderercss (0.86.0): + - React-debug + - React-utils + - React-rendererdebug (0.86.0): + - React-Core-prebuilt + - React-debug + - ReactNativeDependencies + - React-RuntimeApple (0.86.0): + - hermes-engine + - React-callinvoker + - React-Core-prebuilt + - React-Core/Default + - React-CoreModules + - React-cxxreact + - React-featureflags + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-Mapbuffer + - React-NativeModulesApple + - React-RCTFabric + - React-RCTFBReactNativeSpec + - React-RuntimeCore + - React-runtimeexecutor + - React-RuntimeHermes + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - React-RuntimeCore (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-Fabric + - React-featureflags + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-jsitooling + - React-performancetimeline + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - ReactNativeDependencies + - React-runtimeexecutor (0.86.0): + - React-Core-prebuilt + - React-debug + - React-featureflags + - React-jsi (= 0.86.0) + - React-utils + - ReactNativeDependencies + - React-RuntimeHermes (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-featureflags + - React-hermes + - React-jsi + - React-jsinspector + - React-jsinspectorcdp + - React-jsinspectortracing + - React-jsitooling + - React-jsitracing + - React-RuntimeCore + - React-runtimeexecutor + - React-utils + - ReactNativeDependencies + - React-runtimescheduler (0.86.0): + - hermes-engine + - React-callinvoker + - React-Core-prebuilt + - React-cxxreact + - React-debug + - React-featureflags + - React-jsi + - React-jsinspectortracing + - React-performancetimeline + - React-rendererconsistency + - React-rendererdebug + - React-runtimeexecutor + - React-timing + - React-utils + - ReactNativeDependencies + - React-timing (0.86.0): + - React-debug + - React-utils (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-debug + - React-jsi (= 0.86.0) + - ReactNativeDependencies + - React-viewtransitionnativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-Fabric + - React-Fabric/bridging + - React-jsi + - React-jsiexecutor + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - React-webperformancenativemodule (0.86.0): + - hermes-engine + - React-Core-prebuilt + - React-cxxreact + - React-jsi + - React-jsiexecutor + - React-performancetimeline + - React-RCTFBReactNativeSpec + - React-runtimeexecutor + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - ReactAppDependencyProvider (0.86.0): + - ReactCodegen + - ReactCodegen (0.86.0): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-FabricImage + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-NativeModulesApple + - React-RCTAppDelegate + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - ReactCommon (0.86.0): + - React-Core-prebuilt + - ReactCommon/turbomodule (= 0.86.0) + - ReactNativeDependencies + - ReactCommon/turbomodule (0.86.0): + - hermes-engine + - React-callinvoker (= 0.86.0) + - React-Core-prebuilt + - React-cxxreact (= 0.86.0) + - React-jsi (= 0.86.0) + - React-logger (= 0.86.0) + - React-perflogger (= 0.86.0) + - ReactCommon/turbomodule/bridging (= 0.86.0) + - ReactCommon/turbomodule/core (= 0.86.0) + - ReactNativeDependencies + - ReactCommon/turbomodule/bridging (0.86.0): + - hermes-engine + - React-callinvoker (= 0.86.0) + - React-Core-prebuilt + - React-cxxreact (= 0.86.0) + - React-jsi (= 0.86.0) + - React-logger (= 0.86.0) + - React-perflogger (= 0.86.0) + - ReactNativeDependencies + - ReactCommon/turbomodule/core (0.86.0): + - hermes-engine + - React-callinvoker (= 0.86.0) + - React-Core-prebuilt + - React-cxxreact (= 0.86.0) + - React-debug (= 0.86.0) + - React-featureflags (= 0.86.0) + - React-jsi (= 0.86.0) + - React-logger (= 0.86.0) + - React-perflogger (= 0.86.0) + - React-utils (= 0.86.0) + - ReactNativeDependencies + - ReactNativeDependencies (0.86.0) + - RNDeviceInfo (14.0.4): + - React-Core + - RNScreens (4.25.2): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - RNScreens/common (= 4.25.2) + - Yoga + - RNScreens/common (4.25.2): + - hermes-engine + - RCTRequired + - RCTTypeSafety + - React-Core + - React-Core-prebuilt + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - ReactNativeDependencies + - Yoga + - Yoga (0.0.0) + +DEPENDENCIES: + - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) + - RCTRequired (from `../node_modules/react-native/Libraries/Required`) + - RCTSwiftUI (from `../node_modules/react-native/ReactApple/RCTSwiftUI`) + - RCTSwiftUIWrapper (from `../node_modules/react-native/ReactApple/RCTSwiftUIWrapper`) + - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../node_modules/react-native/`) + - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) + - React-Core (from `../node_modules/react-native/`) + - React-Core-prebuilt (from `../node_modules/react-native/React-Core-prebuilt.podspec`) + - React-Core/RCTWebSocket (from `../node_modules/react-native/`) + - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) + - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`) + - React-defaultsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/defaults`) + - React-domnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/dom`) + - React-Fabric (from `../node_modules/react-native/ReactCommon`) + - React-FabricComponents (from `../node_modules/react-native/ReactCommon`) + - React-FabricImage (from `../node_modules/react-native/ReactCommon`) + - React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`) + - React-featureflagsnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/featureflags`) + - React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`) + - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) + - React-idlecallbacksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks`) + - React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) + - React-intersectionobservernativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver`) + - React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`) + - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-jsinspectorcdp (from `../node_modules/react-native/ReactCommon/jsinspector-modern/cdp`) + - React-jsinspectornetwork (from `../node_modules/react-native/ReactCommon/jsinspector-modern/network`) + - React-jsinspectortracing (from `../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) + - React-jsitooling (from `../node_modules/react-native/ReactCommon/jsitooling`) + - React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`) + - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) + - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - React-mutationobservernativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/mutationobserver`) + - react-native-purchasely (from `../node_modules/react-native-purchasely`) + - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-networking (from `../node_modules/react-native/ReactCommon/react/networking`) + - React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`) + - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) + - React-performancecdpmetrics (from `../node_modules/react-native/ReactCommon/react/performance/cdpmetrics`) + - React-performancetimeline (from `../node_modules/react-native/ReactCommon/react/performance/timeline`) + - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`) + - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) + - React-RCTFabric (from `../node_modules/react-native/React`) + - React-RCTFBReactNativeSpec (from `../node_modules/react-native/React`) + - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) + - React-RCTRuntime (from `../node_modules/react-native/React/Runtime`) + - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) + - React-rendererconsistency (from `../node_modules/react-native/ReactCommon/react/renderer/consistency`) + - React-renderercss (from `../node_modules/react-native/ReactCommon/react/renderer/css`) + - React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`) + - React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) + - React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) + - React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) + - React-timing (from `../node_modules/react-native/ReactCommon/react/timing`) + - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) + - React-viewtransitionnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/viewtransition`) + - React-webperformancenativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/webperformance`) + - ReactAppDependencyProvider (from `build/generated/ios/ReactAppDependencyProvider`) + - ReactCodegen (from `build/generated/ios/ReactCodegen`) + - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - ReactNativeDependencies (from `../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`) + - RNDeviceInfo (from `../node_modules/react-native-device-info`) + - RNScreens (from `../node_modules/react-native-screens`) + - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) + +SPEC REPOS: + trunk: + - Purchasely + +EXTERNAL SOURCES: + FBLazyVector: + :path: "../node_modules/react-native/Libraries/FBLazyVector" + hermes-engine: + :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + :tag: hermes-v250829098.0.14 + RCTDeprecation: + :path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" + RCTRequired: + :path: "../node_modules/react-native/Libraries/Required" + RCTSwiftUI: + :path: "../node_modules/react-native/ReactApple/RCTSwiftUI" + RCTSwiftUIWrapper: + :path: "../node_modules/react-native/ReactApple/RCTSwiftUIWrapper" + RCTTypeSafety: + :path: "../node_modules/react-native/Libraries/TypeSafety" + React: + :path: "../node_modules/react-native/" + React-callinvoker: + :path: "../node_modules/react-native/ReactCommon/callinvoker" + React-Core: + :path: "../node_modules/react-native/" + React-Core-prebuilt: + :podspec: "../node_modules/react-native/React-Core-prebuilt.podspec" + React-CoreModules: + :path: "../node_modules/react-native/React/CoreModules" + React-cxxreact: + :path: "../node_modules/react-native/ReactCommon/cxxreact" + React-debug: + :path: "../node_modules/react-native/ReactCommon/react/debug" + React-defaultsnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/defaults" + React-domnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/dom" + React-Fabric: + :path: "../node_modules/react-native/ReactCommon" + React-FabricComponents: + :path: "../node_modules/react-native/ReactCommon" + React-FabricImage: + :path: "../node_modules/react-native/ReactCommon" + React-featureflags: + :path: "../node_modules/react-native/ReactCommon/react/featureflags" + React-featureflagsnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/featureflags" + React-graphics: + :path: "../node_modules/react-native/ReactCommon/react/renderer/graphics" + React-hermes: + :path: "../node_modules/react-native/ReactCommon/hermes" + React-idlecallbacksnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/idlecallbacks" + React-ImageManager: + :path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" + React-intersectionobservernativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/intersectionobserver" + React-jserrorhandler: + :path: "../node_modules/react-native/ReactCommon/jserrorhandler" + React-jsi: + :path: "../node_modules/react-native/ReactCommon/jsi" + React-jsiexecutor: + :path: "../node_modules/react-native/ReactCommon/jsiexecutor" + React-jsinspector: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern" + React-jsinspectorcdp: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/cdp" + React-jsinspectornetwork: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/network" + React-jsinspectortracing: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" + React-jsitooling: + :path: "../node_modules/react-native/ReactCommon/jsitooling" + React-jsitracing: + :path: "../node_modules/react-native/ReactCommon/hermes/executor/" + React-logger: + :path: "../node_modules/react-native/ReactCommon/logger" + React-Mapbuffer: + :path: "../node_modules/react-native/ReactCommon" + React-microtasksnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + React-mutationobservernativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/mutationobserver" + react-native-purchasely: + :path: "../node_modules/react-native-purchasely" + react-native-safe-area-context: + :path: "../node_modules/react-native-safe-area-context" + React-NativeModulesApple: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + React-networking: + :path: "../node_modules/react-native/ReactCommon/react/networking" + React-oscompat: + :path: "../node_modules/react-native/ReactCommon/oscompat" + React-perflogger: + :path: "../node_modules/react-native/ReactCommon/reactperflogger" + React-performancecdpmetrics: + :path: "../node_modules/react-native/ReactCommon/react/performance/cdpmetrics" + React-performancetimeline: + :path: "../node_modules/react-native/ReactCommon/react/performance/timeline" + React-RCTActionSheet: + :path: "../node_modules/react-native/Libraries/ActionSheetIOS" + React-RCTAnimation: + :path: "../node_modules/react-native/Libraries/NativeAnimation" + React-RCTAppDelegate: + :path: "../node_modules/react-native/Libraries/AppDelegate" + React-RCTBlob: + :path: "../node_modules/react-native/Libraries/Blob" + React-RCTFabric: + :path: "../node_modules/react-native/React" + React-RCTFBReactNativeSpec: + :path: "../node_modules/react-native/React" + React-RCTImage: + :path: "../node_modules/react-native/Libraries/Image" + React-RCTLinking: + :path: "../node_modules/react-native/Libraries/LinkingIOS" + React-RCTNetwork: + :path: "../node_modules/react-native/Libraries/Network" + React-RCTRuntime: + :path: "../node_modules/react-native/React/Runtime" + React-RCTSettings: + :path: "../node_modules/react-native/Libraries/Settings" + React-RCTText: + :path: "../node_modules/react-native/Libraries/Text" + React-RCTVibration: + :path: "../node_modules/react-native/Libraries/Vibration" + React-rendererconsistency: + :path: "../node_modules/react-native/ReactCommon/react/renderer/consistency" + React-renderercss: + :path: "../node_modules/react-native/ReactCommon/react/renderer/css" + React-rendererdebug: + :path: "../node_modules/react-native/ReactCommon/react/renderer/debug" + React-RuntimeApple: + :path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios" + React-RuntimeCore: + :path: "../node_modules/react-native/ReactCommon/react/runtime" + React-runtimeexecutor: + :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" + React-RuntimeHermes: + :path: "../node_modules/react-native/ReactCommon/react/runtime" + React-runtimescheduler: + :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" + React-timing: + :path: "../node_modules/react-native/ReactCommon/react/timing" + React-utils: + :path: "../node_modules/react-native/ReactCommon/react/utils" + React-viewtransitionnativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/viewtransition" + React-webperformancenativemodule: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/webperformance" + ReactAppDependencyProvider: + :path: build/generated/ios/ReactAppDependencyProvider + ReactCodegen: + :path: build/generated/ios/ReactCodegen + ReactCommon: + :path: "../node_modules/react-native/ReactCommon" + ReactNativeDependencies: + :podspec: "../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec" + RNDeviceInfo: + :path: "../node_modules/react-native-device-info" + RNScreens: + :path: "../node_modules/react-native-screens" + Yoga: + :path: "../node_modules/react-native/ReactCommon/yoga" + +SPEC CHECKSUMS: + FBLazyVector: b3e7ad108f0d882e30445c5527d774e3fd432f3d + hermes-engine: 7fa7794edc84e91e759bb4fe5095fe8bc63f8ceb + Purchasely: 055a06197235922eb9e888a3d903931996b31add + RCTDeprecation: 2a74a2c57675e64419bd89078efde81f7c1de90b + RCTRequired: 30451112e6fef4e6f31b4e7eee0845156e35e4b0 + RCTSwiftUI: 5aaf0b07e747ba749dc6acc94d8bd41eea4b570f + RCTSwiftUIWrapper: ab2ca548be15d63afa95103afc8685a7c3eab78b + RCTTypeSafety: 3eaed17dbddb0b989208b062ea14c44d412b9780 + React: 2574546f2d017abd14d0c9b48cf2b6a0547c2591 + React-callinvoker: 03cd4b931d1d583d87aae99b8f7b6fe26bf571ee + React-Core: 1c824d9c7dd8aa760b5f1b50d5a54c2a3f598f87 + React-Core-prebuilt: 13924a267683b3d6fa4bde9c80380becf83a9c5c + React-CoreModules: 2f9ed75bca7f6dea2b70e8a1f4a5ca9b6be52d76 + React-cxxreact: 7103d5ba69848c039e11079e74ceede05efe795e + React-debug: 8cc8d99ccc664ef9e873027f7fc3fb0a50496012 + React-defaultsnativemodule: 603c108411d39d7bb5ccb8c270f1f50cb6207e10 + React-domnativemodule: c49a502edc85f515a029c341fe5fba155fa6675c + React-Fabric: acc4915d719db793c5853e5c74b54ea5aa48e865 + React-FabricComponents: aebebc98914a4a8fc962fa7b5b98ebaf250acb68 + React-FabricImage: 42e99e48ff73c8b20cee229e622977d0b97b6247 + React-featureflags: c6d8d8d52acf3a956485726076b73eeff7935340 + React-featureflagsnativemodule: c992de92dcf30dcf09efdec17752abe4128ec55f + React-graphics: 239fd9b0512e539d3563f0825618f4e49795eefd + React-hermes: ae4685ca9fa5f47003bc594d3f146e29284136d1 + React-idlecallbacksnativemodule: 10a5be842ab181953c772a3f28cdf94572833eb0 + React-ImageManager: c36a56c3b13eba92e1d0d30da35d0a1ccf29cd6c + React-intersectionobservernativemodule: 71404bfa47d31e4143cc9db3e26178b5cfcd07e1 + React-jserrorhandler: ca2eeb03c1a77bbfab1b5425b943e3e8d92295f2 + React-jsi: c4b6daf8a31ac54f2db49cd6cce29720fec1f3a8 + React-jsiexecutor: acdc1a217b7ea29bcf6315b770d01e6b1c2fca14 + React-jsinspector: 95d6394efe9fb4a64ed33afc076b32a6384c8514 + React-jsinspectorcdp: 6ce51378a552feaaaac1a7b0d995fa0c49fa4f8c + React-jsinspectornetwork: 0db435c9264f200635fdf294a3b643dd3946d4cf + React-jsinspectortracing: 2e4470e1f301ff597bd65299aa95da4bae6e5c4f + React-jsitooling: 796bc991cdce68e2cc059d0271a28e0d4f8b0891 + React-jsitracing: f3008e7b5e1d9de8d9f2ca1b85824ed86b0cea00 + React-logger: fff73f4ceecad968c97baafdc77dcf84befc38b6 + React-Mapbuffer: ce449ccdf3b80384415b925606be8a4bdcfc65d3 + React-microtasksnativemodule: 2eb3f49d0d8e77b5343455eccd057010b8d38b6b + React-mutationobservernativemodule: f0a0d5ae9b51caf7becbeabf836d716cfedb6bf2 + react-native-purchasely: f9da1c86d6d125c0f0e8084397234e67d8411b15 + react-native-safe-area-context: 3836dc43241ba89903508a442029e57f7491539f + React-NativeModulesApple: a092d89b58f635ebfab88048b0eda9fb516819fd + React-networking: 968bbbe73590149feb1e72b2af4f6a68e4796ece + React-oscompat: 0b72a7e926954a0415ccd83e0748b6561fe45367 + React-perflogger: 865984e492514aa6e5279fc3e663132cfa4d5022 + React-performancecdpmetrics: 2efbf9bdb48c8d8446f4dd10e8bf0dc5d711772f + React-performancetimeline: 9d256484bff1513481be9f234baba694dc3b52e2 + React-RCTActionSheet: 902c79deec52f99cc48b1051b59bcbe86787d339 + React-RCTAnimation: 09cf722039ae30ff5d64e2b011ff054ae651c3b6 + React-RCTAppDelegate: a47de6fddb7eb0c028abba83138a5ce283bd7e77 + React-RCTBlob: 38c418d067e0c61818223503063310122e44c588 + React-RCTFabric: 480ca2a105730e1790cfd13291d086cb53725825 + React-RCTFBReactNativeSpec: 63131378510a5191515a4adfc308e65b465106f4 + React-RCTImage: 3ce36f82441b76b715818ee7ee95f6f5b34f9ee1 + React-RCTLinking: 2f7b5ed4983122e5115732d25a4360960eab583c + React-RCTNetwork: dcd3b180f33da86f5dc5e928a816eb5464fa7f16 + React-RCTRuntime: 6ef8e778fbab426b12eb5a9b5f7a0e86313c5a10 + React-RCTSettings: b97727ec8c55c35bd284457a647c938c040b942b + React-RCTText: 4d1b88f6d3e1a43afe46706d956ec6664c87b984 + React-RCTVibration: 415d14d6a1a64bf947fcef6b193915a494431168 + React-rendererconsistency: ef8519bdd9931261c6561bfad6506356b8108387 + React-renderercss: 1aa1bf99fa2ace143eb87d5190fd43609fc1e832 + React-rendererdebug: 40a4fb3dea21a7ac1759ca0eb6c88a924aecb075 + React-RuntimeApple: 84fadbb4fe8ca531e15e29a22af05911f17569c6 + React-RuntimeCore: f4d9af5f16d37e63308cb03b36c89d01e7f68a06 + React-runtimeexecutor: 4d59410f66af529d04c84c8b152ced07dabc471e + React-RuntimeHermes: fd719d8f4d9ce79636fe2e09e94b0ac31b7b263b + React-runtimescheduler: 784033620aa5515e2f45a60369eed4d32f9400b8 + React-timing: a16df9ae98f950396d9ce3abf43cb0cb2f21194c + React-utils: b68ee619aef28d8f9b85532d98db0327f7bdf743 + React-viewtransitionnativemodule: 11fe091101d381451b1542c37b2745900254f096 + React-webperformancenativemodule: b5d249419f1546663845c82f24de4e18a3180997 + ReactAppDependencyProvider: dcdd0e1b9559a6d8d8aea05286f4ed085091978e + ReactCodegen: afe0d436ee089d7bea6418429d8312b1d23a6801 + ReactCommon: d5c1bb4427bf51c443de5926aac332c89ddd9363 + ReactNativeDependencies: fa0a54b3f5319ae0e3b9aff32bfee7a424b88e66 + RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047 + RNScreens: 991cc417cd396602a6cf59a42139e5a9d91462a9 + Yoga: fe50ab299e578f397fef753cf309c6703a4db29b + +PODFILE CHECKSUM: 55093811dc39d754f7662215c3733976858956e0 + +COCOAPODS: 1.16.2 diff --git a/example/ios/example/AppDelegate.swift b/example/ios/example/AppDelegate.swift index fdf996b4..899814f4 100644 --- a/example/ios/example/AppDelegate.swift +++ b/example/ios/example/AppDelegate.swift @@ -9,9 +9,17 @@ class AppDelegate: RCTAppDelegate { self.moduleName = "example" self.dependencyProvider = RCTAppDependencyProvider() + // E2E mode: launched via `xcrun simctl launch E2E_MODE true` + // (launch argument) or with the E2E_MODE=true environment variable. When set, + // index.js routes the root component to E2ETestRunner (mirrors Android + // MainActivity reading the E2E_MODE intent extra). + let args = ProcessInfo.processInfo.arguments + let env = ProcessInfo.processInfo.environment + let e2eMode = args.contains("E2E_MODE") || env["E2E_MODE"] == "true" + // You can add your custom initial props in the dictionary below. // They will be passed down to the ViewController used by React Native. - self.initialProps = [:] + self.initialProps = e2eMode ? ["e2eMode": true] : [:] return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/example/jest.config.js b/example/jest.config.js index 8eb675e9..294be30f 100644 --- a/example/jest.config.js +++ b/example/jest.config.js @@ -1,3 +1,3 @@ module.exports = { - preset: 'react-native', + preset: '@react-native/jest-preset', }; diff --git a/example/package.json b/example/package.json index 101183ee..8479c604 100644 --- a/example/package.json +++ b/example/package.json @@ -13,37 +13,38 @@ "@purchasely/react-native-purchasely-google": "workspace:*", "@react-navigation/native": "^7.0.14", "@react-navigation/native-stack": "^7.2.0", - "react": "19.0.0", - "react-native": "0.79.2", + "react": "19.2.3", + "react-native": "0.86.0", "react-native-device-info": "^14.0.4", "react-native-purchasely": "workspace:*", "react-native-safe-area-context": "^5.4.0", - "react-native-screens": "^4.9.1" + "react-native-screens": "^4.16.0" }, "devDependencies": { "@babel/core": "^7.29.6", "@babel/preset-env": "^7.25.3", "@babel/runtime": "^7.25.0", - "@react-native-community/cli": "15.0.1", - "@react-native-community/cli-platform-android": "15.0.1", - "@react-native-community/cli-platform-ios": "15.0.1", - "@react-native/babel-preset": "0.79.2", - "@react-native/eslint-config": "0.79.2", - "@react-native/metro-config": "0.79.2", - "@react-native/typescript-config": "0.79.2", + "@react-native-community/cli": "20.1.0", + "@react-native-community/cli-platform-android": "20.1.0", + "@react-native-community/cli-platform-ios": "20.1.0", + "@react-native/babel-preset": "0.86.0", + "@react-native/eslint-config": "0.86.0", + "@react-native/jest-preset": "0.86.0", + "@react-native/metro-config": "0.86.0", + "@react-native/typescript-config": "0.86.0", "@types/jest": "^29.5.13", - "@types/react": "^19.0.0", - "@types/react-test-renderer": "^19.0.0", + "@types/react": "^19.2.0", + "@types/react-test-renderer": "^19.1.0", "eslint": "^8.19.0", "get-yarn-workspaces": "^1.0.2", "jest": "^29.6.3", - "metro-cache": "^0.81.1", - "metro-config": "^0.81.1", + "metro-cache": "^0.84.0", + "metro-config": "^0.84.0", "prettier": "2.8.8", - "react-test-renderer": "19.0.0", - "typescript": "5.0.4" + "react-test-renderer": "19.2.3", + "typescript": "^5.8.3" }, "engines": { - "node": ">=18" + "node": ">=22.11" } } diff --git a/example/src/App.tsx b/example/src/App.tsx index 688bb19b..ad957c1b 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -4,12 +4,12 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack' import { HomeScreen } from './Home.tsx' import Purchasely, { DynamicOffering, - LogLevels, + InterceptResult, PLYDataProcessingLegalBasis, PLYDataProcessingPurpose, - PLYPaywallAction, + PresentationBuilder, PurchaselyUserAttribute, - RunningMode, + removeAllActionInterceptors, } from 'react-native-purchasely' import { PaywallScreen } from './Paywall.tsx' @@ -19,20 +19,23 @@ function App(): React.JSX.Element { async function setupPurchasely() { var configured = false try { - // ApiKey and StoreKit1 attributes are mandatory - configured = await Purchasely.start({ - apiKey: 'fcb39be4-2ba4-4db7-bde3-2a5a1e20745d', - storeKit1: false, // false to use StoreKit 2 and true to use StoreKit 1 - logLevel: LogLevels.DEBUG, // to force log level for debug - userId: 'test-user-id', // if you know your user id - runningMode: RunningMode.FULL, // to set mode manually - }) + // chained builder — the only supported way to start the SDK. + // `allowDeeplink(true)` replaces the legacy `readyToOpenDeeplink`. + configured = await Purchasely.builder( + 'fcb39be4-2ba4-4db7-bde3-2a5a1e20745d' + ) + .appUserId('test-user-id') // if you know your user id + .runningMode('full') // to set mode manually + .logLevel('debug') // to force log level for debug + .allowDeeplink(true) // safe to launch purchase flow from deeplinks + .allowCampaigns(true) + .storekitVersion('storeKit2') // iOS: 'storeKit2' or 'storeKit1' + .stores(['google']) // Android stores + .start() } catch (e) { console.log('Purchasely SDK configuration error:', e) } - // fetchPresentation() - if (!configured) { console.error('Purchasely SDK initialization failed.') } else { @@ -42,9 +45,6 @@ function App(): React.JSX.Element { // logout the user Purchasely.userLogout() - //indicate to sdk it is safe to launch purchase flow - Purchasely.readyToOpenDeeplink(true) - //force your language Purchasely.setLanguage('en') @@ -226,58 +226,47 @@ function App(): React.JSX.Element { const offeringsEmpty: DynamicOffering[] = await Purchasely.getDynamicOfferings() console.log('Dynamic offerings:', offeringsEmpty) - // Set paywall action interceptor callback - Purchasely.setPaywallActionInterceptorCallback((result) => { - console.log('Received action from paywall') - console.log('Action:', result.action) - console.log('Parameters:', result.parameters) - console.log('Info:', result.info) - - - switch (result.action) { - case PLYPaywallAction.NAVIGATE: - console.log( - 'User wants to navigate to website ' + - result.parameters.title + - ' ' + - result.parameters.url - ) - Purchasely.onProcessAction(true) - break - case PLYPaywallAction.LOGIN: - console.log('User wants to login') - //Present your own screen for user to log in - Purchasely.hidePresentation() - // Call this method to display Purchaely paywall - // Purchasely.showPresentation() - // Call this method to update Purchasely Paywall - // Purchasely.onProcessAction(true); - break - case PLYPaywallAction.PURCHASE: - console.log('User wants to purchase') - Purchasely.onProcessAction(true) - //Purchasely.hidePresentation(); - - /** - * If you want to intercept it, hide presentation and display your screen - * then call onProcessAction() to continue or stop purchasely purchase action like this - * - * First hide presentation to display your own screen - * Purchasely.hidePresentation() - * - * Call this method to display Purchasely paywall - * Purchasely.showPresentation() - * - * Call this method to update Purchasely Paywall - * Purchasely.onProcessAction(true|false); // true to continue, false to stop - * - * Purchasely.closePresentation(); //when you want to close the paywall (after purchase for example) - * - **/ - break - default: - Purchasely.onProcessAction(true) + // Set paywall action interceptors. Each handler is typed by the + // action kind and must return 'success' | 'failed' | 'notHandled'. + // Returning 'notHandled' lets the SDK perform its default behavior; + // 'success' tells the SDK the host app fully handled the action. + + // Navigate action — open the requested website yourself if needed. + Purchasely.interceptAction('navigate', async (info, payload): Promise => { + console.log('User wants to navigate', info, payload) + if (payload?.kind === 'navigate') { + console.log( + 'User wants to navigate to website ' + + payload.title + + ' ' + + payload.url + ) } + // Let the SDK open the URL with its default behavior. + return 'notHandled' + }) + + // Login action — present your own login screen here. Returning + // 'success' tells the SDK login is handled by the host app. + Purchasely.interceptAction('login', async (info): Promise => { + console.log('User wants to login', info) + // Present your own screen for the user to log in, then return + // 'success' once done — or 'notHandled' to keep the SDK default. + return 'notHandled' + }) + + // Purchase action — intercept the purchase if you handle it yourself, + // otherwise return 'notHandled' to let Purchasely run the purchase. + Purchasely.interceptAction('purchase', async (info, payload): Promise => { + console.log('User wants to purchase', info, payload) + /** + * To intercept the purchase, present your own screen and run your + * own transaction, then return 'success' or 'failed'. + * Returning 'notHandled' lets Purchasely run its default purchase + * flow. To close the paywall programmatically afterwards, hold a + * reference to the PresentationRequest and call `request.close()`. + **/ + return 'notHandled' }) // Set events listener @@ -297,20 +286,71 @@ function App(): React.JSX.Element { }) } - const fetchPresentation = async () => { + // Preload a placement so its paywall is ready before the user reaches it. + // The `preload()` resolves once the screen is loaded (no UI shown yet). + const preloadOnboarding = async () => { try { - await Purchasely.fetchPresentation({ - placementId: 'ONBOARDING', - contentId: null, - }) + const presentation = await PresentationBuilder.placement( + 'ONBOARDING' + ) + .contentId(null) + .build() + .preload() + console.info('[Purchasely] preloaded', presentation.screenId) } catch (e) { console.error(e) } } + // ------------------------------------------------------------------------- + // presentation demo. Builds an ONBOARDING placement request, wires up + // every lifecycle callback, then displays it. `display()` resolves at + // DISMISS with a 5-field `PLYPresentationOutcome`. + // + // To opt in, uncomment the `presentOnboarding()` call inside the + // `useEffect` below. + // ------------------------------------------------------------------------- + async function presentOnboarding() { + // Build, present and react to lifecycle callbacks. + const request = PresentationBuilder.placement('ONBOARDING') + .contentId('content_123') + .onLoaded((presentation) => { + console.info('[Purchasely] loaded', presentation.screenId) + }) + .onPresented((presentation, error) => { + if (error) { + console.error('[Purchasely] presented error', error) + return + } + console.info('[Purchasely] presented', presentation?.screenId) + }) + .onCloseRequested(() => { + console.info('[Purchasely] close requested by host') + }) + .onDismissed((outcome) => { + console.info( + '[Purchasely] dismissed', + 'purchaseResult=', outcome.purchaseResult, + 'plan=', outcome.plan?.vendorId, + 'closeReason=', outcome.closeReason, + 'error=', outcome.error?.message + ) + }) + .build() + + const outcome = await request.display({ type: 'fullScreen' }) + console.info('[Purchasely] display() resolved with outcome', outcome) + + // Detach every interceptor previously registered. + removeAllActionInterceptors() + } + useEffect(() => { setupPurchasely() - fetchPresentation() + preloadOnboarding() + // Uncomment to display the ONBOARDING paywall through the presentation pipeline + // once the SDK has been started by `setupPurchasely()` above. + // presentOnboarding() }, []) return ( diff --git a/example/src/E2ETestRunner.tsx b/example/src/E2ETestRunner.tsx new file mode 100644 index 00000000..c6b341e4 --- /dev/null +++ b/example/src/E2ETestRunner.tsx @@ -0,0 +1,658 @@ +/** + * E2E test runner — T1–T13 + * + * Renders as the root component when the app is launched with E2E_MODE=true. + * Each test runs sequentially in a single SDK session. + * + * Host-driven tests (require an external driver process): + * T8: [E2E:READY_FOR_TAP] — paywall displayed; host taps the purchase button + * T9: [E2E:READY_FOR_BACK] — paywall displayed; host dismisses it + * Android: adb keyevent BACK via uiautomator + * iOS: xcrun simctl io booted swipe (prepared, not yet active) + * + * Host scripts: + * Android: integration_test/run_e2e.sh (android-emulator-runner + uiautomator) + * iOS: integration_test/run_e2e_ios.sh (prepared, not yet in CI) + * + * Reference: integration_test/E2E_TEST_INDEX.md + */ + +import React, { useEffect, useState } from 'react' +import { + Platform, + ScrollView, + StyleSheet, + Text, + View, +} from 'react-native' +import Purchasely, { + PLYPresentationType, + type PLYPresentationOutcome, + setDefaultPresentationDismissHandler, + removeDefaultPresentationDismissHandler, +} from 'react-native-purchasely' + +// ── Config ─────────────────────────────────────────────────────────────────── +const API_KEY = '0ad0594b-3b3d-4fea-8ee1-4b5df91efe87' +const PLACEMENT_AUDIENCES = 'integration_test_audiences' +const DEEPLINK_AUDIENCES = `ply://ply/placements/${PLACEMENT_AUDIENCES}` + +// ── Types ──────────────────────────────────────────────────────────────────── +type TestStatus = 'pending' | 'running' | 'pass' | 'fail' + +interface TestResult { + id: string + name: string + status: TestStatus + details?: string +} + +// ── Helpers ────────────────────────────────────────────────────────────────── +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +async function waitFor( + fn: () => T | null | undefined, + timeoutMs: number, + intervalMs = 250 +): Promise { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + const val = fn() + if (val != null) return val + await sleep(intervalMs) + } + throw new Error(`Timeout after ${timeoutMs}ms`) +} + +// ── Initial test list ───────────────────────────────────────────────────────── +const INITIAL_TESTS: TestResult[] = [ + { id: 'T1', name: 'getAnonymousUserId — non-empty + UUID format', status: 'pending' }, + { id: 'T2', name: 'isAnonymous: true→login→false→logout→true', status: 'pending' }, + { id: 'T3', name: 'preload → placementId + type + audienceId + plans[].planVendorId', status: 'pending' }, + { id: 'T4', name: 'getDynamicOfferings → array', status: 'pending' }, + { id: 'T5', name: 'allProducts → array', status: 'pending' }, + { id: 'T6', name: 'interceptor register → removeOne → removeAll (no error)', status: 'pending' }, + { id: 'T7', name: 'display(drawer 60%) → close() → outcome: closeReason + presentation props', status: 'pending' }, + { id: 'T8', name: 'purchase interceptor on real tap: plan.vendorId + offer', status: 'pending' }, + { id: 'T9', name: 'defaultDismissHandler + deeplink + BACK → outcome.presentation props', status: 'pending' }, + { id: 'T10', name: 'addEventListener → PRESENTATION_VIEWED: placement_id + sdk_version', status: 'pending' }, + { id: 'T11', name: 'PRESENTATION_CLOSED → placement_id + displayed_presentation', status: 'pending' }, + { id: 'T12', name: 'programmatic close does NOT fire close/closeAll interceptor', status: 'pending' }, + { id: 'T13', name: 'user attributes: set/get string + number + boolean + clear', status: 'pending' }, +] + +// ── Component ───────────────────────────────────────────────────────────────── +export default function E2ETestRunner() { + const [tests, setTests] = useState(INITIAL_TESTS) + const [suiteStatus, setSuiteStatus] = useState<'running' | 'pass' | 'fail' | 'idle'>('idle') + const [log, setLog] = useState([]) + + function updateTest(id: string, patch: Partial) { + setTests((prev) => prev.map((t) => (t.id === id ? { ...t, ...patch } : t))) + } + + function appendLog(line: string) { + setLog((prev) => [...prev.slice(-200), line]) + } + + function pass(id: string, details: string) { + updateTest(id, { status: 'pass', details }) + const msg = `[E2E:${id}:PASS] ${details}` + console.log(msg) + appendLog(`✓ ${id}: ${details}`) + } + + function fail(id: string, error: unknown) { + const msg = error instanceof Error ? error.message : String(error) + updateTest(id, { status: 'fail', details: msg }) + console.error(`[E2E:${id}:FAIL] ${msg}`) + appendLog(`✗ ${id}: ${msg}`) + } + + function running(id: string) { + updateTest(id, { status: 'running' }) + appendLog(`⏳ ${id}…`) + } + + useEffect(() => { + runSuite() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // ── Suite ───────────────────────────────────────────────────────────────── + async function runSuite() { + setSuiteStatus('running') + console.log('[E2E:SUITE:START]') + appendLog('=== Purchasely RN E2E Suite ===') + + // ── SDK init ────────────────────────────────────────────────────────── + let sdkOk = false + try { + // stores() is Android-only; storekitVersion() is iOS-only + const b = Purchasely.builder(API_KEY) + .runningMode('full') + .logLevel('debug') + .allowDeeplink(true) + sdkOk = await ( + Platform.OS === 'android' + ? b.stores(['google']) + : b.storekitVersion('storeKit2') + ).start() + } catch (e) { + console.error('[E2E:INIT:FAIL]', e) + } + if (!sdkOk) { + setSuiteStatus('fail') + console.error('[E2E:SUITE:FAIL] SDK init failed') + appendLog('✗ SDK init failed — aborting suite') + return + } + appendLog('SDK initialized ✓') + + let suitePass = true + + // ── T1 — anonymous user ID ──────────────────────────────────────────── + running('T1') + try { + const id = await Purchasely.getAnonymousUserId() + if (!id || id.length === 0) throw new Error('anonymousUserId is empty') + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + if (!uuidRegex.test(id)) throw new Error(`anonymousUserId not UUID format: ${id}`) + pass('T1', `id=${id}`) + } catch (e) { fail('T1', e); suitePass = false } + + // ── T2 — login / logout cycle ───────────────────────────────────────── + running('T2') + try { + const a1 = await Purchasely.isAnonymous() + if (!a1) throw new Error('Expected isAnonymous=true initially') + await Purchasely.userLogin('rn_it_user') + const a2 = await Purchasely.isAnonymous() + if (a2) throw new Error('Expected isAnonymous=false after login') + Purchasely.userLogout() + const a3 = await Purchasely.isAnonymous() + if (!a3) throw new Error('Expected isAnonymous=true after logout') + pass('T2', 'true→false→true ✓') + } catch (e) { fail('T2', e); suitePass = false } + + // ── T3 — preload: presentation properties ───────────────────────────── + running('T3') + try { + const req = Purchasely.presentation.placement(PLACEMENT_AUDIENCES).build() + const pres = await req.preload() + + if (!pres.screenId || pres.screenId.length === 0) throw new Error('screenId is empty') + if (!pres.placementId) throw new Error('placementId is missing') + if (pres.placementId !== PLACEMENT_AUDIENCES) { + throw new Error(`placementId mismatch: expected "${PLACEMENT_AUDIENCES}", got "${pres.placementId}"`) + } + + // Type must be NORMAL (active placement) or FALLBACK (network issue — still valid) + const validTypes = [PLYPresentationType.NORMAL, PLYPresentationType.FALLBACK] + if (pres.type != null && !validTypes.includes(pres.type)) { + throw new Error(`Unexpected type: ${pres.type} (expected NORMAL or FALLBACK)`) + } + + if (!Array.isArray(pres.plans) || pres.plans.length === 0) { + throw new Error('plans array is empty or missing') + } + const firstPlan = pres.plans[0] + if (!firstPlan?.planVendorId) { + throw new Error(`plans[0].planVendorId missing; plan=${JSON.stringify(firstPlan)}`) + } + + pass( + 'T3', + `screenId=${pres.screenId} placementId=${pres.placementId} ` + + `type=${pres.type} audienceId=${pres.audienceId ?? 'null'} ` + + `plans=${pres.plans.length} plan[0].planVendorId=${firstPlan.planVendorId}` + ) + } catch (e) { fail('T3', e); suitePass = false } + + // ── T4 — dynamic offerings ──────────────────────────────────────────── + running('T4') + try { + const offerings = await Purchasely.getDynamicOfferings() + if (!Array.isArray(offerings)) throw new Error('getDynamicOfferings did not return array') + pass('T4', `count=${offerings.length}`) + } catch (e) { fail('T4', e); suitePass = false } + + // ── T5 — all products ───────────────────────────────────────────────── + running('T5') + try { + const products = await Purchasely.allProducts() + if (!Array.isArray(products)) throw new Error('allProducts did not return array') + pass('T5', `count=${products.length}`) + } catch (e) { fail('T5', e); suitePass = false } + + // ── T6 — interceptor cleanup round-trip ─────────────────────────────── + running('T6') + try { + Purchasely.interceptAction('purchase', async () => 'notHandled' as const) + Purchasely.interceptAction('navigate', async () => 'notHandled' as const) + Purchasely.removeActionInterceptor('purchase') + Purchasely.removeAllActionInterceptors() + pass('T6', 'register→removeActionInterceptor→removeAll ✓') + } catch (e) { fail('T6', e); suitePass = false } + + // ── T7 — display(drawer 60%) + close() → outcome properties ────────── + running('T7') + try { + const req7 = Purchasely.presentation + .placement(PLACEMENT_AUDIENCES) + .build() + + await req7.preload() + + const displayPromise7 = req7.display({ + type: 'drawer', + height: { type: 'percentage', value: 0.6 }, + dismissible: true, + }) + + // Wait 3 s for the drawer to render before programmatic close. + await sleep(3000) + req7.close() + + const outcome7 = await Promise.race([ + displayPromise7, + sleep(15000).then(() => { throw new Error('dismiss timeout after 15 s') }), + ]) + + // Programmatic close → closeReason MUST be exactly 'programmatic' + // (pinned, not merely "one of the valid reasons"), and since no + // purchase happened the outcome's purchaseResult MUST be 'cancelled'. + // This locks the v6 string-union contract on both platforms. + if (outcome7.closeReason !== 'programmatic') { + throw new Error(`closeReason expected 'programmatic', got "${outcome7.closeReason}"`) + } + if (outcome7.purchaseResult !== 'cancelled') { + throw new Error(`purchaseResult expected 'cancelled', got "${outcome7.purchaseResult}"`) + } + if (!outcome7.presentation?.screenId) { + throw new Error(`outcome.presentation.screenId missing; presentation=${JSON.stringify(outcome7.presentation)}`) + } + if (!outcome7.presentation?.placementId) { + throw new Error(`outcome.presentation.placementId missing`) + } + + pass( + 'T7', + `closeReason=${outcome7.closeReason} purchaseResult=${outcome7.purchaseResult} ` + + `presentation.screenId=${outcome7.presentation.screenId} ` + + `presentation.placementId=${outcome7.presentation.placementId}` + ) + } catch (e) { fail('T7', e); suitePass = false } + + await sleep(1000) + + // ── T8 — purchase interceptor: plan + offer on real tap ─────────────── + running('T8') + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let capturedInfo: any = null + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let capturedPayload: any = null + + // Return 'success': we handled it — no native purchase triggered. + Purchasely.interceptAction('purchase', async (info: any, payload: any) => { + capturedInfo = info + capturedPayload = payload + return 'success' as const + }) + + const req8 = Purchasely.presentation + .placement(PLACEMENT_AUDIENCES) + .build() + + req8.display() + + // Wait 3 s for the paywall to render before signalling the host driver. + await sleep(3000) + console.log('[E2E:READY_FOR_TAP]') + appendLog('T8: signaled READY_FOR_TAP — waiting for interceptor…') + + await waitFor(() => capturedPayload, 40000, 300) + + const vendorId: string | undefined = capturedPayload?.plan?.vendorId + if (!vendorId) { + throw new Error( + `payload.plan.vendorId missing; payload=${JSON.stringify(capturedPayload)}` + ) + } + + // offer is the promo offer attached to the purchase action (may be null) + const offer = capturedPayload?.offer ?? null + + pass( + 'T8', + `kind=${capturedPayload?.kind} plan.vendorId=${vendorId} ` + + `plan.storeProductId=${capturedPayload?.plan?.storeProductId ?? 'n/a'} ` + + `offer=${offer != null ? JSON.stringify(offer) : 'none'} ` + + `contentId=${capturedInfo?.contentId ?? 'none'}` + ) + + req8.close() + Purchasely.removeAllActionInterceptors() + } catch (e) { + fail('T8', e) + suitePass = false + Purchasely.removeAllActionInterceptors() + } + + await sleep(1500) + + // ── T9 — defaultDismissHandler + deeplink + BACK → outcome props ────── + running('T9') + try { + let globalOutcome: PLYPresentationOutcome | null = null + + setDefaultPresentationDismissHandler((outcome: PLYPresentationOutcome) => { + globalOutcome = outcome + }) + + const handled = await Purchasely.handleDeeplink(DEEPLINK_AUDIENCES) + if (!handled) throw new Error('handleDeeplink returned false') + + await sleep(2000) + console.log('[E2E:READY_FOR_BACK]') + appendLog('T9: signaled READY_FOR_BACK — waiting for dismiss handler…') + + await waitFor(() => globalOutcome, 40000, 300) + + // Dismissed via system back (Android BACK key / iOS swipe-down) → + // closeReason MUST be exactly 'backSystem' on both platforms. + const reason = globalOutcome!.closeReason + if (reason !== 'backSystem') { + throw new Error(`closeReason expected 'backSystem', got "${reason}"`) + } + if (!globalOutcome!.presentation?.screenId) { + throw new Error(`outcome.presentation.screenId missing`) + } + if (!globalOutcome!.presentation?.placementId) { + throw new Error(`outcome.presentation.placementId missing`) + } + + pass( + 'T9', + `closeReason=${reason} ` + + `presentation.screenId=${globalOutcome!.presentation?.screenId} ` + + `presentation.placementId=${globalOutcome!.presentation?.placementId}` + ) + + removeDefaultPresentationDismissHandler() + } catch (e) { + fail('T9', e) + suitePass = false + removeDefaultPresentationDismissHandler() + } + + await sleep(1000) + + // ── T10 — addEventListener → PRESENTATION_VIEWED ────────────────────── + running('T10') + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let viewedEvent: any = null + const listener10 = Purchasely.addEventListener((event: any) => { + if (event.name === 'PRESENTATION_VIEWED') viewedEvent = event + }) + + const req10 = Purchasely.presentation.placement(PLACEMENT_AUDIENCES).build() + req10.display() + + await waitFor(() => viewedEvent, 15000, 300) + + const placementId10 = viewedEvent.properties?.placement_id + const sdkVersion10 = viewedEvent.properties?.sdk_version + if (!placementId10) { + throw new Error(`PRESENTATION_VIEWED missing placement_id; props=${JSON.stringify(viewedEvent.properties)}`) + } + if (!sdkVersion10) { + throw new Error('PRESENTATION_VIEWED missing sdk_version') + } + + pass( + 'T10', + `PRESENTATION_VIEWED: placement_id=${placementId10} ` + + `sdk_version=${sdkVersion10} ` + + `audience_id=${viewedEvent.properties?.audience_id ?? 'null'}` + ) + + req10.close() + await sleep(500) + listener10.remove() + } catch (e) { fail('T10', e); suitePass = false } + + await sleep(500) + + // ── T11 — PRESENTATION_CLOSED → placement_id + displayed_presentation ─ + running('T11') + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let viewedEvent11: any = null + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let closedEvent11: any = null + const listener11 = Purchasely.addEventListener((event: any) => { + if (event.name === 'PRESENTATION_VIEWED') viewedEvent11 = event + if (event.name === 'PRESENTATION_CLOSED') closedEvent11 = event + }) + + const req11 = Purchasely.presentation.placement(PLACEMENT_AUDIENCES).build() + req11.display() + + // Wait for presentation to appear before closing + await waitFor(() => viewedEvent11, 15000, 300) + await sleep(500) + + req11.close() + + await waitFor(() => closedEvent11, 10000, 300) + + const placementId11 = closedEvent11.properties?.placement_id + const displayedPres11 = closedEvent11.properties?.displayed_presentation + if (!placementId11) { + throw new Error(`PRESENTATION_CLOSED missing placement_id; props=${JSON.stringify(closedEvent11.properties)}`) + } + if (!displayedPres11) { + throw new Error('PRESENTATION_CLOSED missing displayed_presentation') + } + + pass( + 'T11', + `PRESENTATION_CLOSED: placement_id=${placementId11} ` + + `displayed_presentation=${displayedPres11}` + ) + + listener11.remove() + } catch (e) { fail('T11', e); suitePass = false } + + await sleep(500) + + // ── T12 — programmatic close does NOT trigger close/closeAll interceptor + running('T12') + try { + let interceptorCalled = false + + Purchasely.interceptAction('close', async () => { + interceptorCalled = true + return 'notHandled' as const + }) + Purchasely.interceptAction('closeAll', async () => { + interceptorCalled = true + return 'notHandled' as const + }) + + const req12 = Purchasely.presentation.placement(PLACEMENT_AUDIENCES).build() + req12.display() + await sleep(3000) + + // req.close() is programmatic — must bypass the interceptor + req12.close() + await sleep(2000) + + Purchasely.removeAllActionInterceptors() + + if (interceptorCalled) { + throw new Error('close/closeAll interceptor was triggered on programmatic close — unexpected') + } + pass('T12', 'close/closeAll interceptors NOT triggered by req.close() ✓') + } catch (e) { + fail('T12', e) + suitePass = false + Purchasely.removeAllActionInterceptors() + } + + // ── T13 — user attributes: set / get / clear ────────────────────────── + running('T13') + try { + Purchasely.setUserAttributeWithString('e2e_str', 'hello_rn') + Purchasely.setUserAttributeWithNumber('e2e_num', 42) + Purchasely.setUserAttributeWithBoolean('e2e_bool', true) + + await sleep(300) // let native bridge process the set calls + + const strVal = await Purchasely.userAttribute('e2e_str') + const numVal = await Purchasely.userAttribute('e2e_num') + const boolVal = await Purchasely.userAttribute('e2e_bool') + + if (strVal !== 'hello_rn') throw new Error(`str: expected 'hello_rn', got ${JSON.stringify(strVal)}`) + if (numVal !== 42) throw new Error(`num: expected 42, got ${JSON.stringify(numVal)}`) + if (boolVal !== true) throw new Error(`bool: expected true, got ${JSON.stringify(boolVal)}`) + + // Clear and verify + Purchasely.clearUserAttribute('e2e_str') + Purchasely.clearUserAttribute('e2e_num') + Purchasely.clearUserAttribute('e2e_bool') + + await sleep(300) + + const strAfter = await Purchasely.userAttribute('e2e_str') + const numAfter = await Purchasely.userAttribute('e2e_num') + if (strAfter != null) throw new Error(`e2e_str not cleared, got ${JSON.stringify(strAfter)}`) + if (numAfter != null) throw new Error(`e2e_num not cleared, got ${JSON.stringify(numAfter)}`) + + pass('T13', `set: str=hello_rn num=42 bool=true → cleared → null ✓`) + } catch (e) { + fail('T13', e) + suitePass = false + Purchasely.clearUserAttributes() + } + + // ── Final report ────────────────────────────────────────────────────── + setSuiteStatus(suitePass ? 'pass' : 'fail') + if (suitePass) { + console.log('[E2E:SUITE:PASS] All 13 tests passed') + appendLog('=== SUITE PASS ✓ ===') + } else { + console.log('[E2E:SUITE:FAIL] One or more tests failed') + appendLog('=== SUITE FAIL ✗ ===') + } + } + + // ── Render ──────────────────────────────────────────────────────────────── + const suiteBg = + suiteStatus === 'pass' + ? '#2e7d32' + : suiteStatus === 'fail' + ? '#b71c1c' + : '#1565c0' + + return ( + + + + Purchasely RN E2E — {Platform.OS} + + + {suiteStatus === 'idle' && 'Starting…'} + {suiteStatus === 'running' && 'Running…'} + {suiteStatus === 'pass' && '✓ All tests passed'} + {suiteStatus === 'fail' && '✗ Some tests failed'} + + + + {tests.map((t) => ( + + {t.id} + + {t.name} + {t.details && ( + + {t.details} + + )} + + + {t.status === 'pending' && '○'} + {t.status === 'running' && '⟳'} + {t.status === 'pass' && '✓'} + {t.status === 'fail' && '✗'} + + + ))} + + + Log + {log.map((line, i) => ( + + {line} + + ))} + + + ) +} + +// ── Styles ──────────────────────────────────────────────────────────────────── +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#121212' }, + header: { + padding: 20, + paddingTop: 50, + alignItems: 'center', + }, + headerText: { color: '#fff', fontSize: 18, fontWeight: '700' }, + headerSub: { color: '#fff', fontSize: 14, marginTop: 4 }, + testRow: { + flexDirection: 'row', + alignItems: 'center', + padding: 12, + marginHorizontal: 12, + marginTop: 6, + borderRadius: 8, + backgroundColor: '#2c2c2e', + }, + testPass: { backgroundColor: '#1b5e20' }, + testFail: { backgroundColor: '#7f0000' }, + testRunning: { backgroundColor: '#1a237e' }, + testId: { + color: '#fff', + fontWeight: '700', + width: 36, + fontSize: 12, + }, + testBody: { flex: 1, paddingHorizontal: 8 }, + testName: { color: '#eee', fontSize: 13 }, + testDetails: { color: '#aaa', fontSize: 11, marginTop: 2 }, + testIcon: { color: '#fff', fontSize: 16, width: 20, textAlign: 'center' }, + logBox: { + margin: 12, + marginTop: 16, + padding: 12, + backgroundColor: '#1c1c1e', + borderRadius: 8, + }, + logTitle: { color: '#888', fontSize: 11, marginBottom: 6 }, + logLine: { color: '#ccc', fontSize: 11, fontFamily: 'monospace', marginBottom: 2 }, +}) diff --git a/example/src/Home.tsx b/example/src/Home.tsx index eaa8eaa5..aecdf3f5 100644 --- a/example/src/Home.tsx +++ b/example/src/Home.tsx @@ -8,54 +8,37 @@ import { View, } from 'react-native' import { NativeStackScreenProps } from '@react-navigation/native-stack' -import { Colors } from 'react-native/Libraries/NewAppScreen' import Purchasely, { PLYPresentationType, - ProductResult, - PurchaselyPresentation, + PresentationBuilder, + PresentationRequest, } from 'react-native-purchasely' import DeviceInfo from 'react-native-device-info' -import { useEffect, useRef } from 'react' +import { useRef } from 'react' export const HomeScreen: React.FC> = ({ navigation, }) => { const isDarkMode = useColorScheme() === 'dark' - const cachedPresentation = useRef(null) + // Holds the most recently displayed presentation request so it can be closed or + // navigated back programmatically (replaces the v5 show/hide/close calls). + const currentRequest = useRef(null) - useEffect(() => { - Purchasely.fetchPresentation({ - placementId: 'nested', - contentId: null, - }) - .then((p) => { - cachedPresentation.current = p - }) - .catch(console.error) - }, []) const backgroundStyle = { - backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, + backgroundColor: isDarkMode ? '#222222' : '#F3F3F3', } // Boutons d'exemples const buttons = [ { title: 'Display Presentation', onPress: () => onPressPresentation() }, - { title: 'Fetch Presentation', onPress: () => onPressFetch() }, + { title: 'Preload Presentation', onPress: () => onPressPreload() }, { title: 'Display Nested View', onPress: () => onPressNestedView() }, - { - title: 'Show Presentation', - onPress: () => onPressShowPresentation(), - }, - { - title: 'Hide Presentation', - onPress: () => onPressHidePresentation(), - }, { title: 'Close Presentation', onPress: () => onPressClosePresentation(), }, - { title: 'Continue Action', onPress: () => onPressContinueAction() }, + { title: 'Back', onPress: () => onPressBack() }, { title: 'Purchase', onPress: () => onPressPurchase() }, { title: 'Purchase With Promotional Offer', @@ -65,10 +48,6 @@ export const HomeScreen: React.FC> = ({ title: 'Sign Promotional Offer', onPress: () => onPressSignPromotionalOffer(), }, - { - title: 'Display Subscriptions', - onPress: () => onPressSubscriptions(), - }, { title: 'Restore', onPress: () => onPressRestore() }, { title: 'Silent Restore', onPress: () => onPressSilentRestore() }, { title: 'Synchronize', onPress: () => onPressSynchronize() }, @@ -76,23 +55,25 @@ export const HomeScreen: React.FC> = ({ const onPressPresentation = async () => { try { - const result = await Purchasely.presentPresentationForPlacement({ - placementVendorId: 'premium_support', - // isFullscreen: true, - loadingBackgroundColor: '#FFFFFFFF', - }) + // build a request for the placement, then display it. + // `display()` resolves at DISMISS with a PLYPresentationOutcome. + const request = PresentationBuilder.placement('premium_support') + .backgroundColor('#FFFFFFFF') + .build() + currentRequest.current = request - console.log('Result is ' + result.result) + const outcome = await request.display({ type: 'fullScreen' }) - switch (result.result) { - case ProductResult.PRODUCT_RESULT_PURCHASED: - case ProductResult.PRODUCT_RESULT_RESTORED: - if (result.plan != null) { - console.log('User purchased ' + result.plan.name) - } + console.log('Purchase result is ' + outcome.purchaseResult) + switch (outcome.purchaseResult) { + case 'purchased': + case 'restored': + if (outcome.plan != null) { + console.log('User purchased ' + outcome.plan.name) + } break - case ProductResult.PRODUCT_RESULT_CANCELLED: + case 'cancelled': break } } catch (e) { @@ -100,18 +81,22 @@ export const HomeScreen: React.FC> = ({ } } - const onPressFetch = async () => { + const onPressPreload = async () => { try { - const presentation = await Purchasely.fetchPresentation({ - placementId: 'FLOW', - contentId: null, - }) + // preload a placement without showing any UI yet. Resolves once + // the screen is loaded. Inspect the resolved Presentation to decide + // whether to display a Purchasely paywall or your own screen. + const presentation = await PresentationBuilder.placement('FLOW') + .contentId(null) + .build() + .preload() console.log(presentation.placementId) console.log('Type = ' + presentation.type) console.log( 'Plans = ' + JSON.stringify(presentation.plans, null, 2) ) + if (presentation.type === PLYPresentationType.DEACTIVATED) { // No paywall to display return @@ -126,23 +111,25 @@ export const HomeScreen: React.FC> = ({ return } - //Display Purchasely paywall - const result = await Purchasely.presentPresentation({ - presentation: presentation, - }) + // Display the preloaded Purchasely paywall. + const request = PresentationBuilder.placement('FLOW') + .contentId(null) + .build() + currentRequest.current = request + + const outcome = await request.display({ type: 'fullScreen' }) console.log('---- Paywall Closed ----') - console.log('Result is ' + result.result) + console.log('Purchase result is ' + outcome.purchaseResult) - switch (result.result) { - case ProductResult.PRODUCT_RESULT_PURCHASED: - case ProductResult.PRODUCT_RESULT_RESTORED: - if (result.plan != null) { - console.log('User purchased ' + result.plan.name) + switch (outcome.purchaseResult) { + case 'purchased': + case 'restored': + if (outcome.plan != null) { + console.log('User purchased ' + outcome.plan.name) } - break - case ProductResult.PRODUCT_RESULT_CANCELLED: + case 'cancelled': console.log('User cancelled') break } @@ -152,27 +139,20 @@ export const HomeScreen: React.FC> = ({ } const onPressNestedView = () => { + // The embedded PLYPresentationView is driven by a placement id. navigation.navigate('Paywall', { - presentation: cachedPresentation.current, + placementId: 'nested', }) } - const onPressShowPresentation = () => { - Purchasely.showPresentation() - } - - const onPressHidePresentation = () => { - Purchasely.hidePresentation() - } - const onPressClosePresentation = () => { - Purchasely.closePresentation() + // close the currently displayed request programmatically. + currentRequest.current?.close() } - const onPressContinueAction = () => { - //Call this method to continue Purchasely action - Purchasely.showPresentation() - Purchasely.onProcessAction(true) + const onPressBack = () => { + // navigate back inside a multi-step (Flow) presentation. + currentRequest.current?.back() } const onPressPurchase = async () => { @@ -219,10 +199,6 @@ export const HomeScreen: React.FC> = ({ } } - const onPressSubscriptions = () => { - Purchasely.presentSubscriptions() - } - const onPressRestore = async () => { try { const restored = await Purchasely.restoreAllProducts() @@ -242,8 +218,12 @@ export const HomeScreen: React.FC> = ({ } const onPressSynchronize = async () => { - Purchasely.synchronize() - console.log('Synchronize done') + try { + await Purchasely.synchronize() + console.log('Synchronize done') + } catch (e) { + console.error('Synchronize failed', e) + } } return ( diff --git a/example/src/Paywall.tsx b/example/src/Paywall.tsx index 09668f32..0d531052 100644 --- a/example/src/Paywall.tsx +++ b/example/src/Paywall.tsx @@ -5,19 +5,18 @@ import { PLYPresentationView, PresentPresentationResult, ProductResult, - PurchaselyPresentation, } from 'react-native-purchasely' export const PaywallScreen: React.FC> = ({ navigation, route, }) => { - const purchaselyPresentation: PurchaselyPresentation | null = - (route.params as any)?.presentation ?? null + // the embedded PLYPresentationView is driven by a placement id. + const placementId: string | null = + (route.params as any)?.placementId ?? null console.log('### Paywall screen') - console.log('presentation', purchaselyPresentation) - console.log('presentation height : ', purchaselyPresentation?.height) + console.log('placementId', placementId) const callback = (result: PresentPresentationResult) => { console.log('### Paywall closed') @@ -37,10 +36,10 @@ export const PaywallScreen: React.FC> = ({ navigation.goBack() } - if (purchaselyPresentation === null) { + if (placementId === null) { return ( - No presentation (not fetched yet) + No placement provided ) } @@ -61,9 +60,8 @@ export const PaywallScreen: React.FC> = ({