feat!: v6-only paywall API (remove v5) + single native module#243
feat!: v6-only paywall API (remove v5) + single native module#243kherembourg wants to merge 68 commits into
Conversation
|
| Filename | Overview |
|---|---|
| packages/purchasely/ios/PurchaselyRN.m | Merged v6 bridge into main Obj-C module; three issues found: displayPresentation always re-fetches instead of reusing a preloaded request, closePresentation emits spurious CLOSE_REQUESTED on programmatic close, and Close/CloseAll interceptor hardcodes closeReason='button'. |
| packages/purchasely/android/src/main/java/com/reactnativepurchasely/PurchaselyModule.kt | V6 bridge merged into single Kotlin module; correctly reuses preloaded presentations and applies 30s interceptor timeout. Minor: display callback and onDismissed property both call emitPresentationDismissed, contradicting the 'noop' comment. |
| packages/purchasely/src/presentation.ts | New PLYPresentationBuilder/PLYPresentationRequest API with clean lifecycle event handling and teardown on dismiss. normalizePresentation duplicated vs interceptor.ts. |
| packages/purchasely/src/interceptor.ts | New typed interceptAction API; correctly deregisters before re-registering. normalizePresentation duplicated from presentation.ts. |
| packages/purchasely/src/startBuilder.ts | New PurchaselyBuilder cleanly maps string enums to native ordinals and replays cold-start deeplinks. |
| packages/purchasely/src/index.ts | v5 paywall API removed and replaced with v6 builder pattern; core APIs unchanged. |
| packages/purchasely/src/presentationTypes.ts | New cross-platform type contract for the v6 paywall API; well-typed with clear action payload union and outcome types. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant JS as TypeScript (JS)
participant Native as Native Bridge
participant SDK as Purchasely SDK
JS->>Native: preloadPresentation(requestId, payload)
Native->>SDK: PLYPresentationBuilder.build().preload()
SDK-->>Native: LOADED(presentation)
Native-->>JS: PURCHASELY_PRESENTATION_LOADED
JS->>Native: displayPresentation(requestId, payload, transition)
Native->>SDK: display(activity/vc, transition)
SDK-->>Native: onPresented(presentation)
Native-->>JS: PURCHASELY_PRESENTATION_PRESENTED
SDK-->>Native: onDismissed(outcome)
Native-->>JS: PURCHASELY_PRESENTATION_DISMISSED
JS->>JS: resolve(PLYPresentationOutcome)
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant JS as TypeScript (JS)
participant Native as Native Bridge
participant SDK as Purchasely SDK
JS->>Native: preloadPresentation(requestId, payload)
Native->>SDK: PLYPresentationBuilder.build().preload()
SDK-->>Native: LOADED(presentation)
Native-->>JS: PURCHASELY_PRESENTATION_LOADED
JS->>Native: displayPresentation(requestId, payload, transition)
Native->>SDK: display(activity/vc, transition)
SDK-->>Native: onPresented(presentation)
Native-->>JS: PURCHASELY_PRESENTATION_PRESENTED
SDK-->>Native: onDismissed(outcome)
Native-->>JS: PURCHASELY_PRESENTATION_DISMISSED
JS->>JS: resolve(PLYPresentationOutcome)
Prompt To Fix All With AI
Fix the following 5 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 5
packages/purchasely/ios/PurchaselyRN.m:1421-1443
**`closePresentation:` emits `CLOSE_REQUESTED` on programmatic close — wrong semantics on iOS**
`closePresentation:` emits `kPresentationEventCloseRequested` before calling `[presentation close]`. This means every call to `request.close()` from JS fires the `onCloseRequested` builder callback on iOS. Android's `closePresentation()` does NOT emit `EVENT_PRESENTATION_CLOSE_REQUESTED`; it simply calls `closeAllScreens()`. The result is that `onCloseRequested` has inverted semantics across platforms: on Android it fires when the paywall itself wants to close, while on iOS it fires for programmatic close only. An app that registers `onCloseRequested` to detect a user-tap on the paywall close button will receive a false positive on iOS whenever it calls `request.close()`.
### Issue 2 of 5
packages/purchasely/ios/PurchaselyRN.m:1199-1367
**`displayPresentation:` always re-fetches — preloaded presentation is discarded on iOS**
`displayPresentation:` always creates a fresh `PLYPresentationBuilder` and calls `[request preloadWithCompletion:]` from scratch, even when the same `requestId` was already preloaded by a preceding `preloadPresentation:` call. The `kPresentationsByRequest[requestId]` entry written by `preloadPresentation:` is never consulted here. In contrast, the Android bridge checks `activePresentationRequests[requestId]` first and reuses the already-built `PLYPresentationBase.Prepared`. Calling `preload()` followed by `display()` on iOS therefore performs two independent SDK fetches, defeating the purpose of preloading and introducing a window where a backend change could cause the displayed presentation to differ from the preloaded one.
### Issue 3 of 5
packages/purchasely/ios/PurchaselyRN.m:1556-1560
**iOS interceptor always reports `closeReason = "button"` — actual reason silently dropped**
For intercepted `close`/`closeAll` actions, the iOS bridge hardcodes `payloadOut[@"closeReason"] = @"button"`, while the Android bridge correctly maps the real reason via `closeReason.toRNString()`. If the close was triggered by a back-system gesture or programmatically, the interceptor payload on iOS always says `"button"`, giving the handler incorrect data.
```suggestion
case PLYPresentationActionClose:
case PLYPresentationActionCloseAll: {
if (params.closeReason != nil) {
payloadOut[@"closeReason"] = params.closeReason;
} else {
payloadOut[@"closeReason"] = @"button";
}
break;
}
```
### Issue 4 of 5
packages/purchasely/android/src/main/java/com/reactnativepurchasely/PurchaselyModule.kt:806-815
**`DISMISSED` event may fire twice if both `onDismissed` property and `callback` parameter fire**
The comment says "the local `callback` is a noop", but the code passes `callback = { outcome -> emitPresentationDismissed(requestId, outcome) }` — the same function wired via `wirePresentationCallbacks`' `prepared.onDismissed`. If the native SDK fires both callbacks, `DISMISSED` is emitted twice for the same `requestId`. The JS side is safe (subscriptions torn down after first), but the comment contradicts the code. Consider passing an actual noop or removing the duplicate `onDismissed` wiring.
### Issue 5 of 5
packages/purchasely/src/interceptor.ts:27-51
**`normalizePresentation` duplicated between `interceptor.ts` and `presentation.ts`**
The function is defined identically in both files. A diverging update to one copy that is not mirrored in the other will cause subtly different `PLYPresentation` shapes for preload/display vs. interceptor callbacks. Consider extracting it to a shared utility in `presentationTypes.ts`.
Reviews (4): Last reviewed commit: "docs(android): correct setUserAttributeW..." | Re-trigger Greptile
|
All 5 Greptile findings addressed in
Note on #2: I applied the inverse of the literal suggestion (drop event, keep reject rather than drop reject, keep event). Both eliminate the double-fire, but keeping the reject routes through the purpose-built JS Generated by Claude Code |
|
@greptileai review Generated by Claude Code |
|
@greptileai review |
| NSString *callbackId = [[NSUUID UUID] UUIDString]; | ||
| @synchronized (kV6StateLock) { | ||
| kV6InterceptorCallbacks[callbackId] = ^(NSString *result) { | ||
| // Map InterceptResult → bool the native interceptor expects. | ||
| // - success / failed → JS handled the action: don't proceed natively. | ||
| // - notHandled → let the SDK perform its default behavior. | ||
| BOOL proceed = [result isEqualToString:@"notHandled"]; | ||
| onProcessActionHandler(proceed); | ||
| }; |
There was a problem hiding this comment.
iOS interceptor callbacks have no timeout — SDK action can hang indefinitely
The kV6InterceptorCallbacks dictionary stores each callbackId → onProcessActionHandler block with no expiry. If the JS side never calls v6CompleteInterceptor (e.g. the RN bridge is reloaded, the event listener is torn down, or the handler throws before completing), onProcessActionHandler(proceed) is never invoked and the Purchasely SDK action is frozen for the lifetime of the process. The Android bridge addressed this with withTimeoutOrNull(INTERCEPTOR_TIMEOUT_MS = 30_000L) that falls back to NOT_HANDLED. The iOS side needs an equivalent: a GCD dispatch_after (or an NSTimer) that fires after ~30 s, invokes the stored callback with "notHandled" and removes the entry from kV6InterceptorCallbacks.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/purchasely/ios/PurchaselyRNV6.m
Line: 506-514
Comment:
**iOS interceptor callbacks have no timeout — SDK action can hang indefinitely**
The `kV6InterceptorCallbacks` dictionary stores each `callbackId → onProcessActionHandler` block with no expiry. If the JS side never calls `v6CompleteInterceptor` (e.g. the RN bridge is reloaded, the event listener is torn down, or the handler throws before completing), `onProcessActionHandler(proceed)` is never invoked and the Purchasely SDK action is frozen for the lifetime of the process. The Android bridge addressed this with `withTimeoutOrNull(INTERCEPTOR_TIMEOUT_MS = 30_000L)` that falls back to `NOT_HANDLED`. The iOS side needs an equivalent: a GCD `dispatch_after` (or an `NSTimer`) that fires after ~30 s, invokes the stored callback with `"notHandled"` and removes the entry from `kV6InterceptorCallbacks`.
How can I resolve this? If you propose a fix, please make it concise.2177306 to
40faea8
Compare
…ome, interceptor)
Introduces the TypeScript surface for the v6 bridge contract:
- `PresentationBuilder.placement(id) | screen(id) | default()` chain with
`onLoaded`, `onPresented`, `onCloseRequested`, `onDismissed`.
- `PresentationRequest.preload()` and `display()` (resolves at dismiss).
- `PresentationOutcome` (5 fields: presentation, purchaseResult, plan,
closeReason, error) with exclusion rule error ⇒ closeReason == null.
- `Transition`, `InterceptorInfo`, `InterceptResult`, `PresentationActionKind`,
typed `ActionPayload` union.
- `PurchaselyBuilder` start chain (`apiKey().runningMode().allowDeeplink()…`)
exposed via `Purchasely.builder(apiKey)`.
- `Purchasely.interceptAction`, `removeActionInterceptor`,
`removeAllActionInterceptors`.
Legacy v5 APIs (`fetchPresentation`, `setPaywallActionInterceptor`,
`readyToOpenDeeplink`, `setPaywallActionInterceptorCallback`, `start({...})`)
are kept and annotated `@deprecated`.
Bumps the SDK version to 6.0.0 and updates the related test expectations.
All 139 existing tests still pass.
Ref: reports/v6-presentation-comparison-v3-claude/BRIDGE-CONTRACT.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…interceptor)
Adds a new `PurchaselyV6Bridge` helper that maps the v6 cross-platform contract
to the underlying Android v6 SDK:
- `v6Preload(requestId, payload)` / `v6Display(requestId, payload, transition)`
build a `PLYPresentationBase.Prepared` from the JS payload, attach the
`onPresented` / `onCloseRequested` / `onDismissed` callbacks and emit them
through the existing `RCTDeviceEventEmitter` as
`PURCHASELY_V6_{LOADED,PRESENTED,CLOSE_REQUESTED,DISMISSED}`.
- `v6Close(requestId)` / `v6Back(requestId)` provide programmatic control over
the live presentation.
- `v6RegisterInterceptor(kind)` uses the new typed
`Purchasely.interceptAction(actionType, callback)` (Java/`Class<>` overload)
to expose every concrete `PLYPresentationAction` subclass and forwards the
typed payload to JS through `PURCHASELY_V6_ACTION_INTERCEPTED`.
- `v6CompleteInterceptor(callbackId, result)` resolves the suspended
`CompletableDeferred` with the JS-supplied `PLYInterceptResult`.
- `v6UnregisterInterceptor(kind)` calls `Purchasely.removeActionInterceptor`.
- `v6ApplyStartOptions({allowDeeplink, allowCampaigns})` chains the v6 start
options onto the existing `start(...)` native method.
The legacy v5 bridge methods (`fetchPresentation`, `presentPresentation*`,
`setPaywallActionInterceptor`, `onProcessAction`) — whose underlying SDK APIs
are removed in v6 — now reject with a `v6_migration_required` message that
points consumers at the v6 builder. Internal `sendPurchaseResult` is rewritten
on top of `PLYPresentationOutcome` (`sendPurchaseResultV6`). `PLYProductActivity`
is reduced to a stub kept only for the AndroidManifest, and
`PurchaselyViewManager` is rewritten to preload + buildView with v6 APIs.
Bumps the native SDK dependencies (`core`, `google-play`, `huawei-services`,
`amazon`, `player`) to 6.0.0.
Known follow-ups:
- The Android SDK 6.0.0 must be published to Maven before the example app
can build natively.
- `v6Back` is currently a no-op log; the SDK does not expose a per-request
back API yet.
Ref: reports/v6-presentation-comparison-v3-claude/BRIDGE-CONTRACT.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WIP scaffolding for the iOS v6 bridge (PurchaselyRNV6.h declares the category on top of PurchaselyRN). Implementation comes next. Also ignores local caches that polluted git status. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the v6 cross-platform bridge contract on iOS using the existing Purchasely 5.7.4 APIs while the native v6 SDK lands. Adds: - v6Preload / v6Display / v6Close / v6Back exported methods - v6RegisterInterceptor / v6UnregisterInterceptor / v6CompleteInterceptor using the single global setPaywallActionsInterceptor + a kind dispatcher - v6ApplyStartOptions for allowDeeplink/allowCampaigns chain Synthesizes the 5-field outcome (presentation, purchaseResult, plan, closeReason, error) and onPresented(error?) callbacks per the contract workarounds P0.2 / P0.4 / P1.1 — closeReason stays null on iOS until the native pipeline exposes it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Exposes the 5 v6 lifecycle event names (PURCHASELY_V6_LOADED, PRESENTED, CLOSE_REQUESTED, DISMISSED, ACTION_INTERCEPTED) through the RCTEventEmitter supportedEvents array and pulls in the new V6 category header so the methods are linked into the main module. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a v6 builder showcase to the example: PurchaselyBuilder.apiKey() chained start, PresentationBuilder.placement() with onLoaded / onPresented / onCloseRequested / onDismissed callbacks, and a typed 'purchase' interceptor. The legacy v5 setupPurchasely() flow stays the default — the new setupPurchaselyV6() entry is wired but commented out in useEffect so users opt in explicitly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README: add a "Migration to v6.x" section with before/after snippets for init, paywall display, action interceptor and the 5-field outcome - CHANGELOG (new file): document the v6.0.0-beta.0 release contents, the dual API strategy, the iOS workarounds and the deprecated v5 entry points - package.json: bump version to 6.0.0-beta.0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10 tests validating: - PresentationBuilder.placement/screen → v6Preload payload format - screenId → presentationId mapping (P1.1) - display() resolves at DISMISS not at trigger (P0.3) - onPresented synthesizes (null, error) on render fail (P0.4) - Outcome carries 5 fields with closeReason / error mutually exclusive (P0.2) - Action interceptor registry + cross-kind isolation - Orphan events not auto-resolved (native handles timeout) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- gitignore: split the corrupted `.nx/workspace-datajest_dx/` line back into `.nx/workspace-data` + `jest_dx/` (merge dropped the trailing newline). - android: stop double-firing onDismissed on display errors — reject only and let the JS .catch synthesize the dismissed outcome (matches iOS error path). - ios: PresentationBuilder.default() now reads the `isDefault` flag and fetches the default presentation via fetchPresentationWith:nil (fixes 400 in preload + display). - ios: serialise all access to the shared kV6* mutable collections behind @synchronized(kV6StateLock) to avoid RN-thread/main-queue data races. - v6 close(): document + warn that the native SDK has no per-request close yet, so closeAllScreens() dismisses every displayed presentation.
Address the two open Greptile findings on the second review pass of PurchaselyV6Module.kt: - Interceptor timeout (P1, real): wrap `deferred.await()` in `withTimeoutOrNull(INTERCEPTOR_TIMEOUT_MS = 30s)` so the coroutine never suspends indefinitely when JS never calls `completeInterceptor` (e.g. after a bridge reload). On timeout we default to NOT_HANDLED and drop the `pendingInterceptors` entry, so neither the SDK action nor the `complete` lambda is held alive. This fulfils the "native must time out" contract already documented in v6.integration.test.ts. - isDefault on Android (no behaviour change): an empty builder already resolves the default presentation — PLYPresentationManager routes a request with null placementId+presentationId to apiService.getPresentation(null), which substitutes "ply_default". This is the exact mirror of iOS fetchPresentationWith:nil; documented the intentional implicit handling in buildPrepared so it isn't re-flagged. - Tests: lock `default()` -> `isDefault:true` with null placement/presentation ids (guards the iOS isDefault branch added in fbc99b6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BREAKING CHANGE: the legacy v5 paywall API is removed (not deprecated).
There is no soft-transition / dual mode anymore. Paywalls are displayed and
intercepted exclusively through the v6 builders. Version-agnostic core
methods (user, products, subscriptions, attributes, listeners,
presentSubscriptions, clientPresentation*) and the embedded PLYPresentationView
are UNCHANGED.
Removed (TS + iOS + Android):
- start({...}) → Purchasely.builder(apiKey)...start()
- fetchPresentation → presentation.placement(id).build().preload()
- presentPresentation(*) → presentation.placement|screen(id).build().display()
- presentProductWithIdentifier / presentPlanWithIdentifier
→ presentation.screen(id).contentId(c).build().display()
- show/hide/closePresentation → request.display() / request.close()
- setPaywallActionInterceptor(Callback) / onProcessAction
→ interceptAction(kind, handler)
- setDefaultPresentationResultCallback/Handler (TS + iOS)
→ request.onDismissed(outcome => …)
- readyToOpenDeeplink (JS wrapper) → builder(apiKey).allowDeeplink(true).start()
Kept native primitives the v6 layer depends on: native start &
readyToOpenDeeplink (called by the v6 start builder on both platforms);
Android setDefaultPresentationResultHandler (the embedded view manager's
defaultPurchasePromise fallback). iOS removed its variant since the iOS view
uses purchaseResolve directly.
Details:
- TS (src/index.ts): dropped the 16 v5 paywall declarations + now-unused
imports; v6 façade (builder/presentation/interceptAction) is the only
paywall API. Pruned 19 obsolete tests in index.test.ts.
- iOS: removed 12 v5 paywall RCT methods + their exclusive private helpers
and 4 header properties from PurchaselyRN.m/.h; v6 category & view intact;
supportedEvents keeps the merged core+v6 event list.
- Android: removed the v5 paywall @ReactMethods + the orphaned ProductActivity
inner class; deleted PLYProductActivity.kt, its manifest entry and proguard
keep rule; transformPlanToMap & the v6 bridge intact.
- example/: rewritten to the v6 builder/presentation/interceptAction API.
- docs: added MIGRATION-v6.md (old→new mapping) and updated README,
sdk_public_doc.md, CLAUDE.md and CHANGELOG.
Verified: yarn test (133 ✓), yarn typecheck ✓, yarn lint ✓. Native code is
not compilable in this environment (native 6.0.0 SDKs unpublished) and was
verified structurally (grep/brace-balance) + adversarial review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ingle module Removed the standalone `PurchaselyV6Bridge` object (PurchaselyV6Module.kt) and inlined its logic directly into `PurchaselyModule` so the Android side exposes a single native module. - The 8 v6 @ReactMethod entry points (v6Preload/v6Display/v6Close/v6Back/ v6RegisterInterceptor/v6UnregisterInterceptor/v6CompleteInterceptor/ v6ApplyStartOptions) now hold the implementation directly instead of delegating to PurchaselyV6Bridge — the JS contract is unchanged. - v6 helpers (buildV6Prepared, wireV6Callbacks, toV6Map/toV6Payload/toV6String/ toV6Ordinal) are now private members; they reuse the module's existing `sendEvent` and companion `transformPlanToMap` (no duplication). - Event-name constants, the interceptor timeout, and per-request state (activeV6Requests, pendingV6Interceptors) live in the companion object to preserve the process-global semantics the former object singleton had. - Deleted packages/.../reactnativepurchasely/v6/PurchaselyV6Module.kt and the now-empty v6/ package directory. No remaining references to PurchaselyV6Bridge. Verified: brace balance 276/276, no orphaned references, imports complete; yarn test (133 ✓) / typecheck ✓ / lint ✓. Kotlin not compilable in this environment (native 6.0.0 SDK unpublished) — verified structurally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the Android single-module merge on iOS and remove the "v6" branding from the whole codebase now that v6 is the only API. iOS — single bridge: - Merged the PurchaselyRN (V6) category into the main @implementation in PurchaselyRN.m (statics, C helpers, private methods and the 8 RCT bridge methods). Deleted PurchaselyRNV6.h / PurchaselyRNV6.m. One @implementation, braces 298/298. TypeScript — no more v6 folder: - Moved src/v6/{events,interceptor,presentation,startBuilder}.ts to src/ and src/v6/types.ts to src/presentationTypes.ts (avoids the src/types.ts clash). Folded the src/v6/index.ts barrel into src/index.ts. Renamed __tests__/v6.integration.test.ts -> presentation.integration.test.ts. No "v6" mention left in code — synchronized rename across TS + iOS + Android + example + tests (verified: zero [Vv]6 tokens in *.ts/tsx/kt/java/m/h/swift): - Bridge methods: v6Preload->preloadPresentation, v6Display->displayPresentation, v6Close->closePresentation, v6Back->goBackToPreviousScreen, v6RegisterInterceptor->registerActionInterceptor, v6UnregisterInterceptor->unregisterActionInterceptor, v6CompleteInterceptor->completeActionInterceptor, v6ApplyStartOptions->applyStartOptions. - Events: PURCHASELY_V6_LOADED/PRESENTED/CLOSE_REQUESTED/DISMISSED -> PURCHASELY_PRESENTATION_*, PURCHASELY_V6_ACTION_INTERCEPTED -> PURCHASELY_ACTION_INTERCEPTED, PURCHASELY_V6_EVENTS -> PURCHASELY_PRESENTATION_EVENTS, purchaselyV6EventEmitter -> presentationEventEmitter. - All internal v6/V6-prefixed identifiers (iOS kV6*/V6*, Android *V6*, TS V6LifecycleEvent/V6InterceptorEvent) renamed; v6 log tags -> [Purchasely]; NSError domain io.purchasely.v6 -> io.purchasely.presentation. Wire names verified present & matching across TS/iOS/Android. The public API (PresentationBuilder, PurchaselyBuilder, interceptAction, PLYPresentationView) is unchanged. yarn test (133 ✓) / typecheck ✓ / lint ✓. Native verified structurally (brace balance, single @implementation, wire-name sync) — the 6.0.0 SDK is unpublished so it cannot be compiled here. Docs still reference "v6"/6.0.0 as the version/migration concept (out of scope of the code-only rename). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ersion - synchronize(): bridge the native success/error callbacks to a JS Promise (Android onSuccess/onError; iOS was already wired). Source-compatible — the promise resolves on completion and rejects on failure. - Pin io.purchasely:* (core/google-play/player/amazon/huawei) and the Purchasely pod to 6.0.0-rc.1 (published on Maven Central / CocoaPods trunk). - Fix the Android module test: import PLYPresentationType from its v6 package (io.purchasely.ext.presentation), unblocking the native unit-test suite. - Bump all 5 packages + the bridge version to 6.0.0-rc.1. - Docs: MIGRATION-v6 (synchronize awaitable), VERSIONS, sdk_public_doc, V6_MIGRATION_REPORT. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrates the repo from **React Native 0.79.2 → 0.86.0** (example app, the 5 packages' devDeps, and `rn-purchasely-test`). Based on `feat/sdk-v6-migration`. Supersedes #213 (RN 0.83). - `react` 19.0.0 → **19.2.3**, `react-native` 0.79.2 → **0.86.0** - `@react-native/{babel-preset,eslint-config,metro-config,typescript-config,jest-preset}` → **0.86.0** - `@react-native-community/cli` (+ platforms) 15.0.1 → **20.1.0** - `@types/react` → **^19.2.0**, `react-test-renderer` → **19.2.3**, `typescript` → **^5.8.3**, `metro-*` → **^0.84** - Android: Gradle **8.12 → 9.3.1**, buildTools/compileSdk/targetSdk **35 → 36** (minSdk 24, NDK 27.1.12297006, Kotlin 2.1.21 unchanged; New Architecture already enabled). - Node: `.nvmrc` **v20 → v22**, `engines.node >=22.11` (RN 0.86 requirement; CI `setup` reads `.nvmrc`). - Root `resolutions`: `@types/react` → ^19.2.0, dropped obsolete `@types/react-native`. - Jest preset moved out of `react-native` → added `@react-native/jest-preset`; `babel.config.js` → `@react-native/babel-preset` (Hermes-Flow parser). - `postinstall` restores the executable bit on `@react-native-community/cli`'s `build/bin.js` (upstream 20.x packaging bug that breaks Gradle autolinking under Yarn 3). - `react-native-screens` → ^4.16 (`ShadowNode::Shared` removal in RN 0.86). - Added `docs/react-native-upgrade-best-practices.md`. - ✅ `yarn install`, `yarn typecheck`, `yarn lint` - ✅ `yarn test` — 5 suites / 137 tests - ✅ `yarn prepare` (builder-bob lib build) - ✅ example Android `:app:assembleDebug` — BUILD SUCCESSFUL (Gradle 9.3.1, SDK 36, APK generated) - iOS: `pod install` on a compatible Xcode (not run here; pbxproj deployment target unchanged — 15.1 is the RN 0.86 minimum). - `expo-purchasely-test` left on Expo SDK 54 (managed RN) — bump via Expo SDK, not a raw RN pin. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bscriptions The iOS bridge still called native v5 APIs that were removed from Purchasely 6.0.0-rc.1, so the example app failed to compile (errors were masked behind the earlier fmt failure). Migrate the bridge to the v6 builder API: - PurchaselyRN.m / PurchaselyView.swift: PLYPresentationBuilder.forPlacementId/ forScreenId -> build() -> preloadWithCompletion: + onDismissed: (PLYPresentationOutcome); keep showController:type:from: for display; closeDisplayedPresentation -> [presentation close] / closeAllScreens. - RunningMode: TransactionOnly/PaywallObserver removed natively (only Observer=2/ Full=3 remain) -> local PLYRNRunningMode ordinals + runningModeFromOrdinal() mapping, matching Android. - setDynamicOffering: pass the new billingPlanType: argument (Unspecified). - PLYPresentationPlan+Hybrid.m: self.default (ObjC keyword) -> self.default_. BREAKING CHANGE: presentSubscriptions() is removed on iOS and Android (and from the JS surface). The native v6 SDKs no longer ship a built-in subscription-list UI; build your own screen from userSubscriptions(). Verified: yarn typecheck + lint clean, Jest 136/136, and `react-native run-ios --device "iPhone KH"` builds, installs and launches on a physical device. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CHANGELOG: list presentSubscriptions() in the removed-methods section. - MIGRATION-v6.md: presentSubscriptions() is removed on iOS and Android, not a source-compatible no-op. - V6_MIGRATION_REPORT.md: add an addendum superseding doubts #2/#7 — the iOS bridge did not actually compile against rc.1; it is now migrated to the v6 builder API and the example app builds/installs/launches on a physical device. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align the iOS native fallback with the v6 default (observer) and with Flutter's
`PLYRunningMode(rawValue:) ?? .observer`. Previously an unknown/unset ordinal
resolved to Full (mirrored from Android); now only `full` opts into Purchasely
owning the purchase flow. `full`/`transactionOnly` still map to Full,
`observer`/`paywallObserver` to Observer.
Document the default-mode switch in MIGRATION-v6.md as a major, *silent*
behavioural breaking change (no compile error): apps that relied on v5's
implicit full mode must now pass `.runningMode('full')`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the presentSubscriptions removal: the example Home screen still wired a "Display Subscriptions" button to Purchasely.presentSubscriptions(), which no longer exists and would throw at runtime when tapped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align the React Native bridge with the Flutter SDK on the local `6.0.0-beta.12` native build (resolved via mavenLocal), which ships the renamed global handler `Purchasely.setDefaultPresentationDismissHandler`. The previously pinned `6.0.0-rc.1` predates that rename. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
a3c7954 to
b7f4344
Compare
📋 Changements d'API publique React Native — v5 → v6Récapitulatif exhaustif de la surface publique impactée (diff La surface retirée = l'API paywall v5, plus le tracking de présentation client ( 🟢 Ajouté — paywall & démarrage (builders v6)
🟢 Ajouté — cœur (nouvelles méthodes / aliases v6)
🔴 Supprimé → remplacement
🟠 Changements de comportement / de type (à auditer)
✅ Inchangé (cœur du SDK)
Action kinds connus (pour
|
T7: closeReason === 'programmatic' (was: any of the 3) and
purchaseResult === 'cancelled' (was: unchecked) — locks the v6
string-union contract on a programmatic close.
T9: closeReason === 'backSystem' (was: any of the 3) — Android BACK /
iOS swipe-down parity.
Verified iOS locally: T7 closeReason=programmatic purchaseResult=cancelled,
T9 closeReason=backSystem; 13/13 pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror the Android bridge's INTERCEPTOR_TIMEOUT_MS (30_000L): if JS never calls completeActionInterceptor: (RN reload, listener torn down, handler threw before completing), fire the stored callback with notHandled after 30s so the SDK's completion block always runs and the action is never frozen for the process lifetime. Whoever removes the kInterceptorCallbacks entry first (this timer or completeActionInterceptor:) wins; the loser reads nil and no-ops, so the SDK completion can never fire twice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Delete packages/{amazon,android-player,google,huawei}/turbo.json
- Drop 'turbo' devDependency from the same four packages
- Drop dead 'e2eBuild' guard in example/android/app/build.gradle
(property is never set; release APK is built unconditionally via :app:assembleRelease)
- Reconcile yarn.lock (turbo binaries removed)
PR #211 already removed the 'yarn turbo run' invocations from
ci.yml, but the per-package turbo.json + devDep were left behind as
dead code. This finishes the cleanup.
Verified locally:
yarn install: OK (turbo binaries removed)
yarn test: 5 suites / 140 tests passed
yarn typecheck: OK
yarn workspace react-native-purchasely lint: OK
yarn workspace react-native-purchasely prepare: OK
- Restore PLY prefix on all v6 public types to match Flutter & native SDKs (PresentationBuilder→PLYPresentationBuilder, PresentationRequest→PLYPresentationRequest, Presentation→PLYPresentation, Transition→PLYTransition, CloseReason→PLYCloseReason, PurchaseResultKind→PLYPurchaseResult, InterceptResult→PLYInterceptResult, PresentationActionKind→PLYPresentationActionKind, ActionPayload→PLYActionPayload, InterceptorHandler→PLYActionInterceptorHandler, InterceptorInfo→PLYInterceptorInfo, *Payload→PLY*Payload, PresentationError→PLYPresentationError, internal *Event types) - Add PurchaselyBuilder.handleDeeplink(url) for cold-start deeplink replay (parity with Flutter), replayed after start() via existing bridge method - Add campaignId/flowId to PLYPresentation interface + normalizePresentation - Implement back() on iOS (mirror close(): lookup by requestId, respondsToSelector guard) - Sync bridge/sdk version to 6.0.0-rc.2 (was rc.1/stale) - Fix close() JSDoc: iOS does per-request close, Android dismisses all - Remove internal V6_MIGRATION_REPORT.md (PR references MIGRATION-v6.md) - Drop stale turbo.json references from CLAUDE.md Verified: yarn typecheck, yarn test (140/140), yarn lint all green.
Public API parity (Flutter v6) - Add Purchasely.apiKey(...) alias of Purchasely.builder(...) - Add Purchasely.allowDeeplink / allowCampaigns (runtime + builder chain) - Add listener aliases: listenToEvents / stopListeningToEvents, listenToPurchases / stopListeningToPurchases - Add setUserAttributeListener / clearUserAttributeListener - Add setUserAttributeWithInt / setUserAttributeWithDouble and setUserAttributeWithIntArray / setUserAttributeWithDoubleArray - Drop top-level Purchasely.close(), Purchasely.displaySubscriptionCancellationInstruction(), Purchasely.clientPresentationDisplayed / clientPresentationClosed - Drop obsolete types: FetchPresentationParameters, PresentPresentationParameters, PresentPresentationWithIdentifierParameters, PresentPresentationPlacementParameters, PresentProductParameters, PresentPlanParameters, PaywallActionInterceptorResult, PLYPaywallInfo, PresentPresentationResult, FetchPresentationResult - Interceptor normalizeInfo now uses the full PLYPresentation contract (audience, AB-test, campaign, flow, language, plans, metadata, height) Native bridges - Android PurchaselyModule: remove @ReactMethod close, the displaySubscriptionCancellationInstruction handler, the clientPresentationDisplayed / clientPresentationClosed handlers and the FragmentActivity / presentationsLoaded plumbing - Android PurchaselyModule.toRNMap now forwards campaignId and flowId - iOS PurchaselyRN: add allowDeeplink: and allowCampaigns:, keep readyToOpenDeeplink: as a native alias of allowDeeplink:, apply both start options through applyStartOptions:, drop findPresentationLoadedFor / clientPresentationDisplayed / clientPresentationClosed - iOS PurchaselyRN presentationToMap now forwards campaignId and flowId E2E + CI parity (Flutter v6) - Port Flutter T14-T20 to example/src/E2ETestRunner.tsx (extended attribute types, bulk attributes, inc/dec, catalog lookup, dynamic offerings CRUD, screen(id) + modal/popin transitions, config setter smoke test) - run_e2e.sh / run_e2e_ios.sh: T1-T20 + 10 min timeout - e2e-android.yml / e2e-ios.yml: T1-T20 labels, pull_request trigger with path filters, E2E suite now runs on PR - ci.yml: add :react-native-purchasely:testDebugUnitTest after the Android example build so every PR validates the Kotlin bridge - integration_test/E2E_TEST_INDEX.md: T14-T20 documented Build infra / docs - Restore package.json versions (main + google/amazon/huawei/android-player) - MIGRATION-v6.md: v6 helpers (allowCampaigns, listenToEvents, …) and removed APIs - CHANGELOG: 6.0.0-rc.2 entry covering the new helpers, the removals, the native map additions and the E2E/CI changes - PurchaselyModule already pinned to 6.0.0-rc.2 native SDKs; iOS pod spec unchanged
…orrect overload The v6 Purchasely.setUserAttribute signatures are suspend functions with overloads for String[] / Float[] / Boolean[]. When the RN bridge passes an untyped .toTypedArray() result, the suspend bridge throws 'Cannot convert argument of type class [Ljava.lang.String;' on the first array call. Typing the locals explicitly (Array<String>, Array<Float>, Array<Boolean>) makes the Kotlin compiler pick the matching overload instead of failing the JS->native conversion.
The if/else that picked int vs float confused v6's suspend bridge into resolving the String[] overload, throwing 'Cannot convert argument of type class [Ljava.lang.String;' on the first call. Always pass a Float explicitly so the compiler picks the correct overload at compile time (Kotlin won't implicitly convert Double→Float across a suspend call).
…ismissHandler The v5 paywall verbs (fetchPresentation, presentPresentation*, presentProductWithIdentifier, presentPlanWithIdentifier, hide/showPresentation, setPaywallActionInterceptor, onProcessAction) were removed from the public API and the native bridges in v6, leaving only dead jest stubs. Drop them from the central mock and the inline mocks. Add the missing removeDefaultPresentationDismissHandler stub for parity with the public API (index.ts/presentation.ts) and with Flutter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reading an array-typed attribute (String[]/Integer[]/Float[]/Boolean[]) crashed E2E T14 with "Exception in HostFunction: Cannot convert argument of type class [Ljava.lang.String;". Root cause is the read path, not the write: Purchasely.userAttribute() returns a raw Java array, and getUserAttributeValueForRN returned it untouched (else -> value). Under the New Architecture, promise.resolve() hands that raw array to Arguments (Arguments.kt), which rejects it. Normalize Array<*>/List<*> through Arguments.makeNativeArray, converting each element recursively (Int/Float -> Double) so it matches the scalar path and iOS. Adds a red/green regression guard in PurchaselyModuleTest (mockStatic on Purchasely + Arguments): the raw array must never reach promise.resolve(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous comment described a "suspend overload resolved by reflection" theory that turned out to be wrong: the SDK overloads return Deferred (not suspend) and are resolved at compile time. The real reason for value.toFloat() is that RN passes numbers as Double and the SDK has no Double overload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@greptileai review |
There was a problem hiding this comment.
Pull request overview
This PR migrates the Purchasely React Native SDK to a v6-only paywall surface (builder + presentation request lifecycle + typed interceptors), bumps native dependencies to 6.0.0-rc.2, upgrades example/test tooling to React Native 0.86.0, and adds Android/iOS E2E test workflows plus host-side drivers.
Changes:
- Replace v5 paywall APIs with the v6 builder/presentation/interceptor façade (TypeScript + native bridge alignment).
- Upgrade SDK/package versions (JS, iOS pod, Android artifacts) and modernize example + test-project tooling for RN 0.86 / Node 22.
- Add E2E orchestration scripts and CI workflows for Android + iOS device-driven suites.
Reviewed changes
Copilot reviewed 80 out of 83 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| VERSIONS.md | Native SDK version mapping table update (currently inconsistent with rc.2). |
| test-projects/rn-purchasely-test/package.json | Test project dependency + tooling bumps (RN 0.86 / TS 5.8 / Node 22 engines). |
| test-projects/rn-purchasely-test/jest.config.js | Add RN Jest preset config for the test project. |
| scripts/fix-rnc-cli-permissions.js | Postinstall workaround to chmod RN CLI bin for Yarn node-modules linker. |
| README.md | Public README updated to v6 builder/presentation API examples. |
| packages/purchasely/src/types.ts | Remove v5 paywall-related public types; extend presentation plan fields. |
| packages/purchasely/src/startBuilder.ts | New v6 start/builder façade (Purchasely.builder(...).start()). |
| packages/purchasely/src/presentationTypes.ts | New v6 presentation/outcome/interceptor TS contract types. |
| packages/purchasely/src/presentation.ts | New v6 presentation builder/request lifecycle + default dismiss handler wiring. |
| packages/purchasely/src/interfaces.ts | Remove v5 presentation parameter interfaces. |
| packages/purchasely/src/interceptor.ts | New typed action interceptor registry + native completion bridging. |
| packages/purchasely/src/index.ts | Replace v5 API surface with v6-only façade + aliases and exports. |
| packages/purchasely/src/events.ts | New NativeEventEmitter event names and payload typings for v6 lifecycle. |
| packages/purchasely/src/components/PLYPresentationView.tsx | Relax embedded view callback typing (now any) to avoid removed v5 types. |
| packages/purchasely/src/tests/types.test.ts | Remove tests for removed v5 types. |
| packages/purchasely/src/tests/presentation.integration.test.ts | Add integration tests covering v6 presentation + interceptors + outcomes. |
| packages/purchasely/src/tests/index.test.ts | Update SDK tests for v6 builder surface + new aliases. |
| packages/purchasely/src/mocks/testUtils.ts | Update mocks to match new native module surface. |
| packages/purchasely/README.md | Package README rewritten for v6-only initialization + paywall/interceptor usage. |
| packages/purchasely/react-native-purchasely.podspec | Pin iOS native pod dependency to 6.0.0-rc.2. |
| packages/purchasely/package.json | Bump package version to 6.0.0-rc.2 + RN 0.86 dev deps + Jest preset. |
| packages/purchasely/ios/PurchaselyView.swift | Update embedded iOS view to v6 preload/controller install pipeline. |
| packages/purchasely/ios/PurchaselyTests/PurchaselyRNTests.m | Add tests around default dismiss event + exported method. |
| packages/purchasely/ios/PurchaselyRN.h | Update presentationsLoaded typing for v6 protocol-based presentation objects. |
| packages/purchasely/ios/Classes/Hybrid/PLYPresentationPlan+Hybrid.m | Add v6 plan fields (offerVendorId, default). |
| packages/purchasely/CHANGELOG.md | New changelog documenting v6-only surface and breaking removals. |
| packages/purchasely/babel.config.js | Switch preset to @react-native/babel-preset. |
| packages/purchasely/android/src/test/java/com/reactnativepurchasely/PurchaselyModuleTest.kt | Add regression unit test for array attribute conversion via Arguments.makeNativeArray. |
| packages/purchasely/android/src/main/java/com/reactnativepurchasely/PurchaselyViewManager.kt | Refactor embedded Android view manager to v6 preload/outcome model. |
| packages/purchasely/android/src/main/java/com/reactnativepurchasely/PLYSubscriptionsActivity.kt | Delete legacy subscriptions activity (v6 removed surface). |
| packages/purchasely/android/src/main/java/com/reactnativepurchasely/PLYProductActivity.kt | Delete legacy product activity (v6 removed surface). |
| packages/purchasely/android/src/main/AndroidManifest.xml | Remove legacy activity registrations. |
| packages/purchasely/android/consumer-rules.pro | Update proguard rules (currently still references deleted activity). |
| packages/purchasely/android/build.gradle | Pin Android core native dependency to 6.0.0-rc.2. |
| packages/huawei/turbo.json | Remove Turbo task config. |
| packages/huawei/package.json | Bump package to 6.0.0-rc.2; remove Turbo; align RN 0.86 tooling. |
| packages/huawei/babel.config.js | Switch preset to @react-native/babel-preset. |
| packages/huawei/android/build.gradle | Pin Huawei native dependency to 6.0.0-rc.2; add repositories (incl. mavenLocal). |
| packages/google/turbo.json | Remove Turbo task config. |
| packages/google/package.json | Bump package to 6.0.0-rc.2; remove Turbo; align RN 0.86 tooling. |
| packages/google/babel.config.js | Switch preset to @react-native/babel-preset. |
| packages/google/android/build.gradle | Pin Google native dependency to 6.0.0-rc.2; add repositories (incl. mavenLocal). |
| packages/android-player/turbo.json | Remove Turbo task config. |
| packages/android-player/package.json | Bump package to 6.0.0-rc.2; remove Turbo; align RN 0.86 tooling. |
| packages/android-player/babel.config.js | Switch preset to @react-native/babel-preset. |
| packages/android-player/android/build.gradle | Pin player native dependency to 6.0.0-rc.2; add repositories (incl. mavenLocal). |
| packages/amazon/turbo.json | Remove Turbo task config. |
| packages/amazon/package.json | Bump package to 6.0.0-rc.2; remove Turbo; align RN 0.86 tooling. |
| packages/amazon/babel.config.js | Switch preset to @react-native/babel-preset. |
| packages/amazon/android/build.gradle | Pin Amazon native dependency to 6.0.0-rc.2; add repositories (incl. mavenLocal). |
| package.json | Add Node engines >=22.11 and postinstall permission fix; update TS/react typings resolutions. |
| MIGRATION-v6.md | New comprehensive v5 → v6 migration guide. |
| integration_test/tools/tap_purchase.sh | Android host UI driver script for purchase tapping. |
| integration_test/tools/tap_purchase_ios.sh | iOS simulator host UI driver script for purchase tapping (idb). |
| integration_test/tools/swipe_dismiss_ios.sh | iOS simulator host UI driver for dismissing paywalls (tap/swipe). |
| integration_test/tools/press_back.sh | Android host UI driver script for back-dismiss. |
| integration_test/run_e2e.sh | Android E2E orchestrator script (T1–T20). |
| integration_test/run_e2e_ios.sh | iOS simulator E2E orchestrator script (T1–T20). |
| example/src/Paywall.tsx | Example embedded paywall screen updated to placement-driven view. |
| example/src/Home.tsx | Example home updated to v6 request lifecycle (preload/display/close/back). |
| example/src/App.tsx | Example app updated to v6 builder + typed interceptors + preload demo. |
| example/package.json | Example app dependency/tooling bump to RN 0.86 and Node 22 engines. |
| example/jest.config.js | Switch example Jest preset to @react-native/jest-preset. |
| example/ios/Podfile | Add build workaround defines for fmt consteval issues on newer clang/Xcode. |
| example/ios/example/AppDelegate.swift | Route to E2E runner via initial props when launched with E2E flag. |
| example/index.js | Root component routing to E2E runner when e2eMode initial prop is set. |
| example/android/gradle/wrapper/gradle-wrapper.properties | Upgrade Gradle wrapper to 9.3.1. |
| example/android/build.gradle | Bump Android SDK versions to 36 tooling/compile/target. |
| example/android/app/src/main/java/com/purchasely/MainActivity.kt | Add E2E launch options + pause override to avoid Hermes timer freeze. |
| CLAUDE.md | Contributor/AI context updated (currently inconsistent with PR versions). |
| .nvmrc | Update Node version to v22. |
| .gitignore | Add caches/coverage ignore entries. |
| .github/workflows/e2e-ios.yml | New iOS E2E CI workflow. |
| .github/workflows/e2e-android.yml | New Android E2E CI workflow (currently pins Node 20). |
| .github/workflows/ci.yml | CI updated to run Android native unit tests after Android build. |
Comments suppressed due to low confidence (1)
packages/huawei/android/build.gradle:60
- mavenLocal() being first in repositories can cause non-reproducible dependency resolution (local artifacts overriding the published ones). For a published integration package, it’s usually safer to rely on google()/mavenCentral()/Huawei repo only.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| -keep class com.reactnativepurchasely.PurchaselyModule { *; } | ||
| -keep class com.reactnativepurchasely.PurchaselyPackage { *; } | ||
| -keep class com.reactnativepurchasely.PLYProductActivity { *; } | ||
| -keep class com.reactnativepurchasely.PLYSubscriptionsActivity { *; } | ||
|
|
| | 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 | |
| | 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) | |
| - name: Setup Node.js 20 | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '20' | ||
| cache: 'yarn' |
| repositories { | ||
| mavenLocal() | ||
| google() | ||
| mavenCentral() | ||
| } |
| repositories { | ||
| mavenLocal() | ||
| google() | ||
| mavenCentral() | ||
| } |
| repositories { | ||
| mavenLocal() | ||
| google() | ||
| mavenCentral() | ||
| } |
io.purchasely:*:6.0.0-rc.2 requires minSdk 23; keeping 21 in the RN packages breaks consumer manifest merging. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
….2 surface - versions: 6.0.0-rc.2 everywhere (npm, native iOS/Android pins), RN 0.86, TS 5.8.3, Node v22, minSdk 23; VERSIONS.md gains the rc.2 row and the 5.7.3 line is back in chronological order - MIGRATION-v6.md: real exported type names (PLYPresentationBuilder, PLYPresentationRequest), defaultSource() entry point, presentation builder options (backgroundColor/progressColor/displayCloseButton/ displayBackButton with per-platform semantics), full transition types and dimensions, cold-start handleDeeplink builder modifier, and the PLYLoadedPresentation lifecycle (display/close/back) - drop the last unprefixed/legacy enum references (PLYPaywallAction, 4-value RunningMode) from CLAUDE.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…presentation builder The JS presentation builder has been sending these keys since v6, and the Android bridge already consumes them (PurchaselyModule.kt); the iOS bridge silently dropped them. Tri-state guard mirrors Android: absent key or JSON null leaves the backend-defined visibility untouched. Note iOS semantics are suppression-only (only false hides a backend-shown button). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- PLYPresentationBuilder.defaultSource() is the canonical cross-platform entry point (default() stays as the iOS-matching alias) - preload() now resolves a PLYLoadedPresentation exposing display(transition?)/close()/back() delegating to its origin request, mirroring the Flutter loaded-presentation lifecycle - drop the legacy v5 PLYPaywallAction enum (snake_case, open_flow_step); PLYPresentationActionKind is the only action vocabulary - reduce RunningMode to OBSERVER/FULL — the v6 natives removed TransactionOnly and PaywallObserver - expose a public read-only requestId getter on PLYPresentationRequest - document the real displayCloseButton/displayBackButton platform semantics (Android toggle, iOS suppression-only) BREAKING CHANGE: PLYPaywallAction is removed and RunningMode no longer declares TRANSACTION_ONLY/PAYWALL_OBSERVER. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t={...}>
Port of PR #254 onto the aligned v6 API. The component accepts a
PLYPresentationRequest preloaded upstream and reuses its native
presentation by requestId on both platforms (iOS
loadedPresentationForRequestId, Android PurchaselyModule.loadedPresentation),
falling back to placementId/presentation. onPresentationClosed is now
typed with the exported PLYPresentationViewResult ({ result, plan }, which
is what the native embedded view actually emits). Example gains a
PaywallPreloaded demo screen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…handler Align the dismiss routing rule with Flutter/native (local onDismissed ?? global default handler): a presentation displayed by the app whose request has no local onDismissed now delivers its outcome to the handler registered via setDefaultPresentationDismissHandler, in addition to resolving the display() promise. A local onDismissed still wins and suppresses the default routing for that presentation. The native DEFAULT_DISMISSED event only fires for SDK-opened presentations (campaign/deeplink/Promoted IAP) which carry no requestId, so each dismissal reaches the default handler through exactly one path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the seven scenarios present in the Flutter E2E suite but missing here: - T21 synchronize() settles (resolve or clean store rejection, no hang) - T22 default dismiss handler catches a fire-and-forget display() - T23 local onDismissed wins over the default handler - T24 user attribute listener set/removed events - T25 embedded <PLYPresentationView request> renders (close best-effort, per Flutter INLINE_PAYWALL_CLOSE.md findings) - T26 PLYLoadedPresentation display/close lifecycle - T27 cold-start deeplink via builder .handleDeeplink() before start(): Android runs it as a dedicated fresh-process phase (E2E_PHASE intent extra relaunch in run_e2e.sh); iOS emits an explicit [E2E:T27:SKIP] (no launch-arg phase bridge in the example AppDelegate yet) Adds a skip status and [E2E:Tn:SKIP] marker, extends both host scripts' report loops to T21-T27, and documents everything (steps, asserts, divergences vs Flutter, cross-wrapper mapping) in E2E_TEST_INDEX.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The path filters only watched integration_test/, the example runner and build.gradle/podspec files, so a pure bridge change (packages/purchasely JS, iOS or Android source) never triggered the E2E workflows. Add packages/purchasely/src/**, packages/purchasely/ios/** and packages/purchasely/android/src/** to push and pull_request filters of both workflows, matching how Flutter gates its E2E on purchasely/lib/**. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mounting <PLYPresentationView request={...}> hard-froze the whole app
(CI E2E T25): Fabric instantiated the PurchaselyView manager on the main
thread while the JS effect concurrently resolved the same module via
NativeModules.PurchaselyView; with requiresMainQueueSetup=true the JS
thread dispatch_sync'd onto the blocked main queue — a module-holder
deadlock proven by process samples (main thread in
condition_variable::wait, JS thread in RCTUnsafeExecuteOnMainQueueSync).
requiresMainQueueSetup now returns false; view() is still main-queue.
Also rework the controller mounting to the pattern proven in the Flutter
NativeView: frame-based layout resynced in layoutSubviews (no Auto Layout
against the Yoga host), addChild/didMove containment on the root VC, and
a BALANCED appearance transition driven from didMoveToWindow (the old
begin-without-end left viewDidAppear unfired), with symmetric teardown.
The iOS SDK only emits PRESENTATION_VIEWED through its own full-screen
display flow, so a manually embedded controller rendered without ever
surfacing the event (Android's SDK does fire it for embedded views). The
view now emits it to JS listeners when the paywall actually appears —
cross-platform parity, no backend analytics involved, and no double-fire
possible since the full-screen path never goes through PurchaselyView.
Verified locally: full iOS E2E suite green (T1-T26 PASS, T27 SKIP).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Podfile.lock finally records the local pod at 6.0.0-rc.2 (podspec was bumped earlier without regenerating the lock); the pbxproj/Info.plist changes (RCT_REMOVE_LEGACY_ARCH, PODFILE_DIR, RCTNewArchEnabled) are injected automatically by the React Native CocoaPods scripts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Migrates the React Native SDK to the v6 paywall API as the single, only paywall surface, unifies the native layer into one module per platform, pins the now‑published native 6.0.0‑rc.2 SDKs, moves the example/tooling to React Native 0.86.0, and adds end‑to‑end test suites (T1–T20) on both Android and iOS with their own CI workflows.
Paywall API — v6 only
PurchaselyBuilder(start),PLYPresentationBuilder→PLYPresentationRequest(preload/display/close/back/onDismissed) resolving to a 5‑fieldPLYPresentationOutcome, and typedinterceptAction. No dual/soft‑transition mode.setDefaultPresentationDismissHandler/removeDefaultPresentationDismissHandleradded: a global handler that receives thePLYPresentationOutcomewhen a paywall opened outside an explicitdisplay()(campaign, deeplink, Promoted IAP) is dismissed.clientPresentationDisplayed/clientPresentationClosedand the globalclose()are gone (superseded byrequest.close()).PLYPresentationViewall keep their existing API. New v6 aliases/capabilities were added on top (allowCampaigns,allowDeeplink,listenToEvents/listenToPurchases,setUserAttributeListener, typedsetUserAttributeWithInt/Double(Array)).PLYPresentationBuilder/interceptActionAPI.MIGRATION-v6.md.Single native module per platform
PurchaselyRN.m;PurchaselyRNV6.h/.mdeleted. One Objective‑C bridge.PurchaselyV6Bridgemerged intoPurchaselyModule.kt; thev6/package is gone. One Kotlin module."v6"naming left in the code (TypeScriptsrc/v6/dissolved intosrc/, bridge method/event names and log tags cleaned).v6survives only as the SDK version and in the migration guide. Public API names are unchanged.Native dependencies (published)
Pinned to the released 6.0.0‑rc.2 native SDKs — the native builds are no longer blocked:
Purchasely 6.0.0-rc.2.io.purchasely:core/google-play/amazon/huawei-services/playerall6.0.0-rc.2.React Native 0.86.0
Example app and test‑project tooling migrated to RN 0.86.0 (New Architecture / bridgeless). CI iOS therefore requires Xcode ≥ 16.1 → the iOS E2E job runs on
macos-15.End‑to‑end tests (T1–T20)
A device‑driven E2E suite (
example/src/E2ETestRunner.tsx, launched viaE2E_MODE) exercises the real native bridge end‑to‑end:PRESENTATION_VIEWED/PRESENTATION_CLOSEDevents, base user attributes.allowDeeplink/allowCampaigns/setLanguage/setThemeMode/setLogLevel/setDebugMode/revokeDataProcessingConsent).See
integration_test/E2E_TEST_INDEX.md..github/workflows/e2e-android.yml:ubuntu-latest+ KVM + x86_64 emulator (ReactiveCircus/android-emulator-runner), release APK (JS bundle embedded). PR + manual + nightly..github/workflows/e2e-ios.yml:macos-15simulator, Release build (JS bundle embedded, no Metro), markers captured viasimctl launch --console, T8/T9 driven withidb(point coordinates). PR + manual + nightly.Breaking changes
The v5 paywall methods are removed:
start({...}),fetchPresentation,presentPresentation*,presentProductWithIdentifier,presentPlanWithIdentifier,show/hide/closePresentation, globalclose(),setPaywallActionInterceptor(Callback),onProcessAction,setDefaultPresentationResultCallback/Handler,readyToOpenDeeplink,presentSubscriptions,clientPresentationDisplayed/Closed, andisDeeplinkHandled(renamedhandleDeeplink). Behaviour:runningModenow defaults to'observer'(was effectivelyfull) — a silent change apps must audit. See the pinned comment andMIGRATION-v6.mdfor the full old → new mapping. Core (user/products/subscriptions/attributes/listeners) is not affected.Test plan
yarn typecheck/yarn lint— cleanyarn test— unit suite green (139 tests)build-android(incl.:react-native-purchasely:testDebugUnitTest) /build-ios— green in CIe2e-android.yml)e2e-ios.yml)Reference
MIGRATION-v6.mdintegration_test/E2E_TEST_INDEX.mdpackages/purchasely/CHANGELOG.md🤖 Generated with Claude Code