feat: migrate to Purchasely 6.0 native SDK (6.0.0-rc.1)#56
Conversation
- plugin.xml (both): plugin version 6.0.0-rc.1; iOS pod Purchasely 6.0.0-rc.2; Android io.purchasely:core + google-play 6.0.0-rc.2 - package.json / package-lock (main + example): 6.0.0-rc.1 - www/Purchasely.js cordovaSdkVersion fallback 6.0.0-rc.1 - VERSIONS.md: add 6.0.0-rc.1 -> 6.0.0-rc.2 / 6.0.0-rc.2 row Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- start(options, success, error): single config object instead of positional args
- RunningMode values are now name strings ('observer'/'full'); paywallObserver kept
as a deprecated alias -> 'observer' (native iOS/Android enum raw values differ)
- synchronize(success, error) now reports completion
- deeplink renames: allowDeeplink / handleDeeplink (+ new allowCampaigns); old
readyToOpenDeeplink / isDeeplinkHandled kept as deprecated JS aliases
- setDefaultPresentationDismissHandler (setDefaultPresentationResultHandler alias)
- presentation display mode string replaces isFullscreen bool (bool normalized);
add backPresentation(); remove presentSubscriptions, presentProduct/PlanWithIdentifier,
show/hidePresentation (no v6 primitive)
- interceptor: onProcessAction accepts InterceptResult string (legacy bool mapped);
PaywallAction kept, add PresentationAction alias
- new constants: InterceptResult, PresentationType, CloseReason, TransitionType,
DimensionType, Store, StorekitVersion
- update __tests__/Purchasely.test.js in lockstep (80 tests pass)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- start(options) config object; RunningMode/Store constants - allowDeeplink / handleDeeplink; setDefaultPresentationDismissHandler - interceptor uses PresentationAction + InterceptResult; fix closePaywall bug - display-mode strings; add backPresentation; wire openDeeplink button - remove presentSubscriptions / show / hide presentation buttons+handlers Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ci.yml: actions/checkout@v7, setup-node@v5, setup-java@v5, cache@v5; Node 24; add workflow_dispatch + concurrency (cancel-in-progress) - publish.yml: checkout@v7, setup-node@v5; publish prerelease tags (X.Y.Z-*) under npm 'next' dist-tag, stable under 'latest' Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- MIGRATION-v6.md: start(options), RunningMode (strings/observer default), synchronize completion, display-mode strings, removed methods, InterceptResult interceptor, deeplink renames, dismiss handler rename, new constants, @next install - README (root + purchasely): 6.0 start(options) + display-mode examples, migration pointer (also fixes stale startWithAPIKey in purchasely/README) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- PurchaselyPlugin.kt migrated to io.purchasely:core 6.0.0-rc.2 (mirrors Flutter v6):
start() parses one options dict; runningMode mapped by name (Observer/Full);
PLYPresentationBase.builder().build().preload() -> loaded.display(activity, transition){outcome};
fetchPresentation stashes Loaded for later display; closePresentation -> closeAllScreens();
add backPresentation(); interceptAction() for all kinds -> single callback + onProcessAction(string);
setDefaultPresentationDismissHandler; allowDeeplink/handleDeeplink/allowCampaigns;
synchronize reports success/error; CoroutineScope for suspend queries; isEligibleToOffer rename
- remove PLYProductActivity/PLYSubscriptionsActivity (+ layouts/themes) — dropped in native 6.0;
drop their plugin.xml source-file + AndroidManifest activity entries and stale example config
- build-extras.gradle: Java 11 / kotlin jvmTarget 11 (core 6.0 ships Java 11 bytecode)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CDVPurchasely.m migrated to the v6 @objc API (verified against generated Purchasely-Swift.h; no Swift shim needed): start() parses one options dict -> PurchaselyBuilder chain -> startWithInitialized:; PLYPresentationBuilder forScreenId:/forPlacementId: -> build -> preloadWithCompletion: / display; displayFrom:transitionType:(PLYDisplayMode); closeAllScreens; backPresentation; interceptAction:handler: for all kinds -> single callback + onProcessAction(string -> PLYInterceptResult); setDefaultPresentationDismissHandler; allowDeeplink/ handleDeeplink/allowCampaigns; synchronize success/error; PLYPresentationOutcome serialization (result int, closeReason, error, presentation) - remove presentSubscriptions / presentPlan|ProductWithIdentifier / hide|showPresentation - fix v6 type changes: PLYAttribute is an enum (was pointer) in setAttribute:; declare PLYEventDelegate/PLYUserAttributeDelegate conformance on the categories that implement them; guard user-attribute callbacks on attributeCommand (v6 delegate setters are _Nonnull, no native unregister) Verified: iOS example builds against Purchasely 6.0.0-rc.2 (BUILD SUCCEEDED). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- revert build-extras.gradle Java-11 override: cordova-android 15 already compiles at Java 17 (>= 11), which consumes io.purchasely:core 6.0.0-rc.2 (Java 11 bytecode); the kotlin tasks.withType override failed to resolve in the cordova gradle context - example devDeps/lock refreshed by platform add (cordova-android 15, cordova-ios 8.1); plugin version fields stay 6.0.0-rc.1 Verified: Android example builds app-debug.apk against io.purchasely:core 6.0.0-rc.2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors the Flutter integration_test suite (E2E_TEST_INDEX) adapted to the Cordova
imperative API, driving the real Purchasely 6.0 native SDK.
- purchasely/example/e2e: WebdriverIO project (Appium uiautomator2 + xcuitest drivers)
- WEBVIEW-context bridge suite (hard gate): getAnonymousUserId, allProducts,
fetchPresentationForPlacement, synchronize completion, user-attribute round-trip
- dismiss suite (best-effort): present placement + programmatic close -> closeReason
- helpers/driver.js (context switch + bridge invoker), runner scripts with 3x retry
and Flutter-style gating (hard bridge, best-effort dismiss), README
- .github/workflows/e2e-android.yml (emulator-runner, API 34) + e2e-ios.yml (simulator)
on workflow_dispatch + nightly + scoped PRs; upload ci-logs artifacts
- CLAUDE.md: document e2e workflows + rc npm dist-tag
Note: device suites need the E2E placement to exist for com.purchasely.demo on the
example API key's backend; the deterministic bridge suite is the hard gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR migrates the Purchasely Cordova plugin to the v6 native SDKs (iOS
|
| Filename | Overview |
|---|---|
| purchasely/src/android/PurchaselyPlugin.kt | Full v6 migration: builder+preload+display replaces Activity-based flow, typed interceptors, CoroutineScope for suspend calls — but loadedPresentations entries are never removed after presentPresentation displays them, leaking native objects. |
| purchasely/src/ios/CDVPurchasely.m | v6 iOS migration to PLYPresentationBuilder+preload+display, per-action interceptors, PLYPresentationOutcome serialization — fetchPresentation has a silent hang on nil/nil completion, and the presentPresentation controller nil-guard may be broken for preloaded v6 presentations. |
| purchasely/www/Purchasely.js | New options-based start(), RunningMode by name, synchronize with callbacks, deeplink renames with backward-compat aliases, new v6 constants — all cleanly implemented with 80/80 tests. |
| purchasely/plugin.xml | Removed PLYProductActivity/PLYSubscriptionsActivity entries; bumped native deps to 6.0.0-rc.2 on both platforms; clean removal of deleted layout and theme files. |
| purchasely/src/ios/CDVPurchasely.h | Header updated for v6: PLYPresentation instead of controller-based, PLYInterceptResult completion block, new backPresentation/allowDeeplink/allowCampaigns methods. |
| purchasely/tests/Purchasely.test.js | 80 tests covering new v6 constants, renamed methods, deprecated aliases, and changed start signature — comprehensive coverage of the JS layer changes. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant JS as JavaScript
participant Bridge as Cordova Bridge
participant Native as Native SDK (iOS/Android)
JS->>Bridge: start(options)
Bridge->>Native: Purchasely.Builder(options).build().start()
Native-->>Bridge: error? / success
Bridge-->>JS: success(true) / error(msg)
JS->>Bridge: fetchPresentation(placementId, contentId)
Bridge->>Native: PLYPresentationBase.builder().placementId().build().preload()
Native-->>Bridge: Loaded presentation (stored in map/array)
Bridge-->>JS: "success(presentationDict{id, type, plans...})"
JS->>Bridge: presentPresentation(presentationDict, displayMode)
Bridge->>Native: loaded.display(activity, transition)
Note over Bridge,Native: Android: map entry NOT removed
Native-->>Bridge: PLYPresentationOutcome (on dismiss)
Bridge-->>JS: "success({result, plan, closeReason, presentation})"
JS->>Bridge: setPaywallActionInterceptor()
Bridge->>Native: interceptAction(kind, handler) x10 kinds
Native-->>Bridge: PLYInterceptorInfo + action payload
Bridge-->>JS: "success({action, info, parameters})"
JS->>Bridge: "onProcessAction(success|failed|notHandled)"
Bridge->>Native: completion(PLYInterceptResult)
%%{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 JavaScript
participant Bridge as Cordova Bridge
participant Native as Native SDK (iOS/Android)
JS->>Bridge: start(options)
Bridge->>Native: Purchasely.Builder(options).build().start()
Native-->>Bridge: error? / success
Bridge-->>JS: success(true) / error(msg)
JS->>Bridge: fetchPresentation(placementId, contentId)
Bridge->>Native: PLYPresentationBase.builder().placementId().build().preload()
Native-->>Bridge: Loaded presentation (stored in map/array)
Bridge-->>JS: "success(presentationDict{id, type, plans...})"
JS->>Bridge: presentPresentation(presentationDict, displayMode)
Bridge->>Native: loaded.display(activity, transition)
Note over Bridge,Native: Android: map entry NOT removed
Native-->>Bridge: PLYPresentationOutcome (on dismiss)
Bridge-->>JS: "success({result, plan, closeReason, presentation})"
JS->>Bridge: setPaywallActionInterceptor()
Bridge->>Native: interceptAction(kind, handler) x10 kinds
Native-->>Bridge: PLYInterceptorInfo + action payload
Bridge-->>JS: "success({action, info, parameters})"
JS->>Bridge: "onProcessAction(success|failed|notHandled)"
Bridge->>Native: completion(PLYInterceptResult)
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 4
purchasely/src/android/PurchaselyPlugin.kt:576-601
**Missing `loadedPresentations` cleanup after display**
After `presentPresentation` retrieves and displays a preloaded presentation, the entry is never removed from `loadedPresentations`. Every successful `fetchPresentation` call adds a `PLYPresentationBase.Loaded` object to the map, but nothing removes it once `presentPresentation` is done. The old v5 code explicitly called `presentationsLoaded.removeAll { it.id == presentation.id && it.placementId == presentation.placementId }` after display. With high-frequency calls (e.g., onboarding flows that fetch and display repeatedly), native presentation objects accumulate indefinitely, leaking memory.
Add `loadedPresentations.remove(handle)` after successfully retrieving the loaded presentation in `presentPresentation`.
### Issue 2 of 4
purchasely/src/android/PurchaselyPlugin.kt:585-590
The preloaded presentation should be removed from `loadedPresentations` once it has been retrieved for display, mirroring the iOS cleanup (`[self.presentationsLoaded removeObjectAtIndex:index]`) and the v5 `presentationsLoaded.removeAll` pattern. Without removal, repeated `fetchPresentation`+`presentPresentation` calls grow the map indefinitely.
```suggestion
val handle = if (presentationMap.has("id")) presentationMap.optString("id") else null
val loaded = handle?.let { loadedPresentations.remove(it) }
if (loaded == null) {
callbackContext.error("presentation cannot be found")
return
}
```
### Issue 3 of 4
purchasely/src/ios/CDVPurchasely.m:1003-1012
**`fetchPresentation` command hangs when completion returns `nil, nil`**
The `preloadWithCompletion:` callback handles `error != nil` (calls `failureFor:`) and `presentation != nil` (calls `successFor:`), but has no branch for the case where both are `nil`. In v6, a deactivated placement or a race condition where the SDK cannot build a presentation can produce a `nil, nil` completion. When that happens, neither `failureFor:` nor `successFor:` is ever sent, leaving the Cordova command permanently pending — the JS success/error callbacks are never invoked.
Add an `else` branch after the `else if (presentation != nil)` block that calls `failureFor:command resultString:@"Presentation unavailable"` (or similar).
### Issue 4 of 4
purchasely/src/ios/CDVPurchasely.m:1031-1035
**`presentationLoaded.controller` may always be `nil` before v6 display**
The guard `presentationLoaded.controller == nil` was valid in v5 where the controller was set during `preloadWithCompletion`. In v6, the `PLYPresentation` returned from `preloadWithCompletion` may not populate `controller` until `displayFrom:transitionType:` is called. If the v6 SDK lazily sets `controller` on first display, this guard will always fail with "Presentation not loaded" for every pre-fetched presentation, making `fetchPresentation`→`presentPresentation` completely non-functional on iOS. Could you confirm whether `id<PLYPresentation>.controller` is non-nil after v6 preload (before display)?
Reviews (1): Last reviewed commit: "test(e2e): add Appium + WebdriverIO devi..." | Re-trigger Greptile
| private fun presentPresentation(presentationMap: JSONObject?, | ||
| isFullScreen: Boolean, | ||
| displayMode: String?, | ||
| loadingBackgroundColor: String?, | ||
| callbackContext: CallbackContext) { | ||
| if (presentationMap == null) { | ||
| callbackContext.error("presentation cannot be null") | ||
| return | ||
| } | ||
|
|
||
| val presentation = presentationsLoaded.lastOrNull { | ||
| it.id == presentationMap.getString("id") | ||
| && it.placementId == presentationMap.getString("placementId") | ||
| } | ||
| if(presentation == null) { | ||
| val handle = if (presentationMap.has("id")) presentationMap.optString("id") else null | ||
| val loaded = handle?.let { loadedPresentations[it] } | ||
| if (loaded == null) { | ||
| callbackContext.error("presentation cannot be found") | ||
| return | ||
| } | ||
|
|
||
| purchaseCallback = callbackContext | ||
| displayedPresentation = loaded | ||
|
|
||
| cordova.activity.let { activity -> | ||
| if (presentation.flowId != null) { | ||
| presentation.display(activity) { result, plan -> | ||
| sendPurchaseResult(result, plan) | ||
| } | ||
| } else { | ||
| // Open legacy Activity for now if not a flow | ||
| val intent = PLYProductActivity.newIntent(activity, PLYPresentationProperties(), isFullScreen, loadingBackgroundColor).apply { | ||
| putExtra("presentation", presentation) | ||
| } | ||
| activity.startActivity(intent) | ||
| // TODO(v6-verify): loadingBackgroundColor cannot be applied to an already-preloaded | ||
| // presentation in v6 (background/progress colors are builder options set before preload). | ||
| cordova.activity?.runOnUiThread { | ||
| loaded.display(cordova.activity, transitionForDisplayMode(displayMode)) { outcome -> | ||
| sendPurchaseResult(outcome) | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing
loadedPresentations cleanup after display
After presentPresentation retrieves and displays a preloaded presentation, the entry is never removed from loadedPresentations. Every successful fetchPresentation call adds a PLYPresentationBase.Loaded object to the map, but nothing removes it once presentPresentation is done. The old v5 code explicitly called presentationsLoaded.removeAll { it.id == presentation.id && it.placementId == presentation.placementId } after display. With high-frequency calls (e.g., onboarding flows that fetch and display repeatedly), native presentation objects accumulate indefinitely, leaking memory.
Add loadedPresentations.remove(handle) after successfully retrieving the loaded presentation in presentPresentation.
Prompt To Fix With AI
This is a comment left during a code review.
Path: purchasely/src/android/PurchaselyPlugin.kt
Line: 576-601
Comment:
**Missing `loadedPresentations` cleanup after display**
After `presentPresentation` retrieves and displays a preloaded presentation, the entry is never removed from `loadedPresentations`. Every successful `fetchPresentation` call adds a `PLYPresentationBase.Loaded` object to the map, but nothing removes it once `presentPresentation` is done. The old v5 code explicitly called `presentationsLoaded.removeAll { it.id == presentation.id && it.placementId == presentation.placementId }` after display. With high-frequency calls (e.g., onboarding flows that fetch and display repeatedly), native presentation objects accumulate indefinitely, leaking memory.
Add `loadedPresentations.remove(handle)` after successfully retrieving the loaded presentation in `presentPresentation`.
How can I resolve this? If you propose a fix, please make it concise.| val handle = if (presentationMap.has("id")) presentationMap.optString("id") else null | ||
| val loaded = handle?.let { loadedPresentations[it] } | ||
| if (loaded == null) { | ||
| callbackContext.error("presentation cannot be found") | ||
| return | ||
| } |
There was a problem hiding this comment.
The preloaded presentation should be removed from
loadedPresentations once it has been retrieved for display, mirroring the iOS cleanup ([self.presentationsLoaded removeObjectAtIndex:index]) and the v5 presentationsLoaded.removeAll pattern. Without removal, repeated fetchPresentation+presentPresentation calls grow the map indefinitely.
| val handle = if (presentationMap.has("id")) presentationMap.optString("id") else null | |
| val loaded = handle?.let { loadedPresentations[it] } | |
| if (loaded == null) { | |
| callbackContext.error("presentation cannot be found") | |
| return | |
| } | |
| val handle = if (presentationMap.has("id")) presentationMap.optString("id") else null | |
| val loaded = handle?.let { loadedPresentations.remove(it) } | |
| if (loaded == null) { | |
| callbackContext.error("presentation cannot be found") | |
| return | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: purchasely/src/android/PurchaselyPlugin.kt
Line: 585-590
Comment:
The preloaded presentation should be removed from `loadedPresentations` once it has been retrieved for display, mirroring the iOS cleanup (`[self.presentationsLoaded removeObjectAtIndex:index]`) and the v5 `presentationsLoaded.removeAll` pattern. Without removal, repeated `fetchPresentation`+`presentPresentation` calls grow the map indefinitely.
```suggestion
val handle = if (presentationMap.has("id")) presentationMap.optString("id") else null
val loaded = handle?.let { loadedPresentations.remove(it) }
if (loaded == null) {
callbackContext.error("presentation cannot be found")
return
}
```
How can I resolve this? If you propose a fix, please make it concise.| id<PLYPresentationRequest> request = [builder build]; | ||
| [request preloadWithCompletion:^(id<PLYPresentation> _Nullable presentation, NSError * _Nullable error) { | ||
| if (error != nil) { | ||
| [self failureFor:command resultString: error.localizedDescription]; | ||
| } else if (presentation != nil) { | ||
| [self.presentationsLoaded addObject:presentation]; | ||
| [self successFor:command resultDict:[self resultDictionaryForFetchPresentation:presentation]]; | ||
| } | ||
| }]; | ||
| }); |
There was a problem hiding this comment.
fetchPresentation command hangs when completion returns nil, nil
The preloadWithCompletion: callback handles error != nil (calls failureFor:) and presentation != nil (calls successFor:), but has no branch for the case where both are nil. In v6, a deactivated placement or a race condition where the SDK cannot build a presentation can produce a nil, nil completion. When that happens, neither failureFor: nor successFor: is ever sent, leaving the Cordova command permanently pending — the JS success/error callbacks are never invoked.
Add an else branch after the else if (presentation != nil) block that calls failureFor:command resultString:@"Presentation unavailable" (or similar).
Prompt To Fix With AI
This is a comment left during a code review.
Path: purchasely/src/ios/CDVPurchasely.m
Line: 1003-1012
Comment:
**`fetchPresentation` command hangs when completion returns `nil, nil`**
The `preloadWithCompletion:` callback handles `error != nil` (calls `failureFor:`) and `presentation != nil` (calls `successFor:`), but has no branch for the case where both are `nil`. In v6, a deactivated placement or a race condition where the SDK cannot build a presentation can produce a `nil, nil` completion. When that happens, neither `failureFor:` nor `successFor:` is ever sent, leaving the Cordova command permanently pending — the JS success/error callbacks are never invoked.
Add an `else` branch after the `else if (presentation != nil)` block that calls `failureFor:command resultString:@"Presentation unavailable"` (or similar).
How can I resolve this? If you propose a fix, please make it concise.| if (presentationLoaded == nil || presentationLoaded.controller == nil) { | ||
| [self failureFor:command resultString: @"Presentation not loaded"]; | ||
| return; | ||
| } | ||
|
|
There was a problem hiding this comment.
presentationLoaded.controller may always be nil before v6 display
The guard presentationLoaded.controller == nil was valid in v5 where the controller was set during preloadWithCompletion. In v6, the PLYPresentation returned from preloadWithCompletion may not populate controller until displayFrom:transitionType: is called. If the v6 SDK lazily sets controller on first display, this guard will always fail with "Presentation not loaded" for every pre-fetched presentation, making fetchPresentation→presentPresentation completely non-functional on iOS. Could you confirm whether id<PLYPresentation>.controller is non-nil after v6 preload (before display)? In the v6 iOS SDK, does a PLYPresentation returned from preloadWithCompletion: have a non-nil controller property before displayFrom:transitionType: is called?
Prompt To Fix With AI
This is a comment left during a code review.
Path: purchasely/src/ios/CDVPurchasely.m
Line: 1031-1035
Comment:
**`presentationLoaded.controller` may always be `nil` before v6 display**
The guard `presentationLoaded.controller == nil` was valid in v5 where the controller was set during `preloadWithCompletion`. In v6, the `PLYPresentation` returned from `preloadWithCompletion` may not populate `controller` until `displayFrom:transitionType:` is called. If the v6 SDK lazily sets `controller` on first display, this guard will always fail with "Presentation not loaded" for every pre-fetched presentation, making `fetchPresentation`→`presentPresentation` completely non-functional on iOS. Could you confirm whether `id<PLYPresentation>.controller` is non-nil after v6 preload (before display)? In the v6 iOS SDK, does a `PLYPresentation` returned from `preloadWithCompletion:` have a non-nil `controller` property before `displayFrom:transitionType:` is called?
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Pull request overview
Migrates the Purchasely Cordova plugin bridge to Purchasely 6.0 native SDKs (iOS/Android RC), aligning the JavaScript API with the new v6 start/presentation/interceptor contracts and updating CI + docs accordingly.
Changes:
- Updated JS bridge API:
start(options), display-mode strings (with legacy boolean normalization), interceptor result enum, deeplink/dismiss-handler renames + deprecated aliases. - Migrated iOS (Objective-C) and Android (Kotlin) native implementations to Purchasely v6 presentation + interceptor APIs; removed obsolete Android Activities/resources.
- Added Appium + WebdriverIO E2E scaffolding and updated CI/publish workflows and documentation for the 6.0 RC line.
Reviewed changes
Copilot reviewed 43 out of 53 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| VERSIONS.md | Adds v6.0.0-rc.1 → native v6.0.0-rc.2 version mapping. |
| README.md | Updates top-level usage docs for start(options) and display-mode transition type. |
| purchasely/www/Purchasely.js | Updates JS bridge API for v6 (start/options, transitions, interceptor result, deeplink/dismiss handler renames). |
| purchasely/src/ios/CDVPurchasely+UserAttributes.m | Guards user-attribute callbacks when listener is not registered. |
| purchasely/src/ios/CDVPurchasely+UserAttributes.h | Declares v6 PLYUserAttributeDelegate conformance on the implementing category. |
| purchasely/src/ios/CDVPurchasely+Events.h | Declares v6 PLYEventDelegate conformance on the implementing category. |
| purchasely/src/ios/CDVPurchasely.m | Migrates iOS bridge to Purchasely v6 start/presentation/interceptor APIs and outcome serialization. |
| purchasely/src/ios/CDVPurchasely.h | Updates iOS plugin surface/types for v6 (presentation tracking, interceptor completion signature). |
| purchasely/src/android/theme_purchasely_fullscreen.xml | Removes obsolete fullscreen theme (dropped with removed Activities). |
| purchasely/src/android/theme_purchasely_fullscreen_v23.xml | Removes obsolete v23 fullscreen theme. |
| purchasely/src/android/theme_purchasely_fullscreen_v23_light_status_bar.xml | Removes obsolete v23 light-status-bar fullscreen theme. |
| purchasely/src/android/PurchaselyPlugin.kt | Migrates Android bridge to Purchasely v6 APIs (start options, presentations, synchronize callbacks, interceptors, coroutine-based queries). |
| purchasely/src/android/PLYSubscriptionsActivity.java | Removes deprecated subscriptions Activity (no longer in v6). |
| purchasely/src/android/PLYProductActivity.kt | Removes deprecated product Activity (no longer in v6). |
| purchasely/src/android/activity_ply_subscriptions_activity.xml | Removes obsolete subscriptions Activity layout. |
| purchasely/src/android/activity_ply_product_activity.xml | Removes obsolete product Activity layout. |
| purchasely/README.md | Updates package-level docs for v6 usage and display-mode argument. |
| purchasely/plugin.xml | Bumps plugin + native dependency versions; removes obsolete Android Activity/resource entries. |
| purchasely/package.json | Bumps wrapper version to 6.0.0-rc.1. |
| purchasely/package-lock.json | Bumps lock/package version to 6.0.0-rc.1. |
| purchasely/example/www/js/index.js | Updates example app to v6 JS API and new interceptor result + deeplink/dismiss handlers. |
| purchasely/example/www/index.html | Updates example UI to remove deprecated actions and add back navigation. |
| purchasely/example/package.json | Updates Cordova platform versions used by the example. |
| purchasely/example/package-lock.json | Updates example lockfile to new Cordova platform versions and plugin versions. |
| purchasely/example/e2e/wdio.shared.conf.js | Adds shared WDIO config for E2E suites. |
| purchasely/example/e2e/wdio.ios.conf.js | Adds iOS simulator WDIO config. |
| purchasely/example/e2e/wdio.android.conf.js | Adds Android emulator WDIO config. |
| purchasely/example/e2e/tools/ci_run_e2e.sh | Adds Android E2E runner script with hard/best-effort gating. |
| purchasely/example/e2e/tools/ci_run_e2e_ios.sh | Adds iOS E2E runner script with hard/best-effort gating. |
| purchasely/example/e2e/specs/dismiss.e2e.js | Adds best-effort dismiss/outcome E2E coverage. |
| purchasely/example/e2e/specs/bridge.e2e.js | Adds hard-gated deterministic WEBVIEW bridge E2E coverage. |
| purchasely/example/e2e/README.md | Documents E2E suites, gating policy, and local/CI requirements. |
| purchasely/example/e2e/package.json | Adds Appium + WebdriverIO dev dependencies for E2E. |
| purchasely/example/e2e/helpers/driver.js | Adds helper utilities for context switching and bridge invocation in E2E. |
| purchasely/example/e2e/.gitignore | Ignores E2E node_modules/logs/lockfile. |
| purchasely/example/config.xml | Removes obsolete Android edit-config for removed Activity theme override. |
| purchasely/tests/Purchasely.test.js | Updates unit tests for v6 JS API + new exported constants and renamed methods. |
| purchasely-google/plugin.xml | Bumps Google Play companion plugin version + dependency to v6 RC. |
| purchasely-google/package.json | Bumps Google Play companion plugin version to 6.0.0-rc.1. |
| MIGRATION-v6.md | Adds a dedicated migration guide for 5.x → 6.0 JS API changes. |
| CLAUDE.md | Updates CI/publish notes including prerelease next dist-tag behavior and E2E workflow references. |
| .idea/markdown.xml | Adds IDE markdown preview settings (project metadata). |
| .idea/inspectionProfiles/Project_Default.xml | Adds IDE inspection profile (project metadata). |
| .idea/copilot.data.migration.edit.xml | Adds IDE Copilot migration state (project metadata). |
| .idea/copilot.data.migration.ask2agent.xml | Adds IDE Copilot migration state (project metadata). |
| .idea/copilot.data.migration.agent.xml | Adds IDE Copilot migration state (project metadata). |
| .github/workflows/publish.yml | Updates publish workflow, including prerelease next dist-tag publishing logic. |
| .github/workflows/e2e-ios.yml | Adds iOS E2E workflow for real-backend Appium/WDIO runs. |
| .github/workflows/e2e-android.yml | Adds Android E2E workflow for real-backend Appium/WDIO runs. |
| .github/workflows/ci.yml | Updates CI workflow triggers, concurrency, action versions, and Node version. |
Files not reviewed (8)
- .idea/caches/deviceStreaming.xml: Generated file
- .idea/copilot.data.migration.agent.xml: Generated file
- .idea/copilot.data.migration.ask2agent.xml: Generated file
- .idea/copilot.data.migration.edit.xml: Generated file
- .idea/inspectionProfiles/Project_Default.xml: Generated file
- .idea/markdown.xml: Generated file
- purchasely/example/package-lock.json: Generated file
- purchasely/package-lock.json: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function normalizeDisplayMode(mode) { | ||
| if (mode === true) return 'fullScreen'; | ||
| if (mode === false) return 'modal'; | ||
| return mode || 'fullScreen'; | ||
| } |
| var fn = window.Purchasely[method]; | ||
| if (typeof fn !== 'function') { | ||
| return finish({ ok: false, error: 'no such method: ' + method }); | ||
| } | ||
| fn.apply( | ||
| window.Purchasely, | ||
| args.concat([ | ||
| function (value) { finish({ ok: true, value: value }); }, | ||
| function (error) { finish({ ok: false, error: String(error) }); }, | ||
| ]) | ||
| ); |
| val handle = if (presentationMap.has("id")) presentationMap.optString("id") else null | ||
| val loaded = handle?.let { loadedPresentations[it] } | ||
| if (loaded == null) { | ||
| callbackContext.error("presentation cannot be found") | ||
| return | ||
| } | ||
|
|
||
| purchaseCallback = callbackContext | ||
| displayedPresentation = loaded |
Add the v6 per-action interceptor surface across all three layers, matching the native SDK (which intercepts per action) and the RN/Flutter contract: - JS: interceptAction(kind, handler) returning an InterceptResult, plus removeActionInterceptor(kind) / removeAllActionInterceptors(). The old setPaywallActionInterceptor + onProcessAction are kept as a deprecated shim over the new machinery. - iOS/Android: registerActionInterceptor / unregisterActionInterceptor / completeActionInterceptor, keyed by a per-invocation callbackId. This fixes the previous single-stashed-completion limitation — concurrent intercepts now resolve independently. - Update example app, MIGRATION-v6.md, and unit tests (85/85 pass). Cleanup (removes 4023 lines of noise): - Remove 6 committed .idea/ files and purchasely-google/.DS_Store. - Revert purchasely/yarn.lock to main's stub (spurious yarn install bloat; the project uses package-lock.json). - Add a root .gitignore so these can't return. Verified: Android :app:compileDebugKotlin and iOS xcodebuild both succeed against Purchasely 6.0.0-rc.2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The JS CloseReason enum listed iOS's values {none, button,
interactive_dismiss, programmatic}, but native Android PLYCloseReason
emits back_system (button/back_system/programmatic), which was absent
from the enum — Android apps could not match it, and the two bridges
diverged.
Align with the native wire contract shared with the Flutter plugin's
PLYCloseReason:
- JS enum -> {button, backSystem:'back_system', programmatic}
- iOS resultDictionaryForOutcome: Button->button,
InteractiveDismiss->back_system (matches the SDK's own convention,
Purchasely-Swift.h: "interactiveDismiss stringifies to back_system"),
Programmatic->programmatic, None-> omit key (mirrors Flutter's null
closeReason on iOS)
- Android unchanged (already emits the canonical values, verified
against core-6.0.0-rc.2)
- Update unit test (85/85 pass) and MIGRATION-v6.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flutter PR #120 removed the v5 compatibility aliases outright; mirror that clean break in the Cordova bridge instead of keeping shims. Removed from the JS API: - readyToOpenDeeplink -> allowDeeplink - isDeeplinkHandled -> handleDeeplink - setDefaultPresentationResultHandler -> setDefaultPresentationDismissHandler - setPaywallActionInterceptor + onProcessAction -> interceptAction(kind, handler) - RunningMode.paywallObserver -> observer - PaywallAction constant -> PresentationAction (now the canonical name) Also drop the legacy boolean interceptor-result normalization (handlers now return an InterceptResult). Native bridges are unchanged — the aliases were JS-only and delegated to the retained native actions. Updated the example app, unit tests (77 pass), and MIGRATION-v6.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Migrates the Purchasely Cordova plugin to the Purchasely 6.0 native SDKs (iOS
Purchasely 6.0.0-rc.2, Androidio.purchasely:core 6.0.0-rc.2), mirroring the Flutter migration in Purchasely-Flutter#120. Wrapper version →6.0.0-rc.1.This is a migration, not a rewrite — the breaking surfaces are SDK start, presentation display mode, and the action-interceptor result; the rest stays source-compatible (with deprecated aliases for renamed methods).
What changed
JavaScript (
purchasely/www/Purchasely.js)start(apiKey, …)→start(options, success, error)config objectRunningModevalues are now name strings (observer/full) — the native iOS/Android enums use different raw values, so the bridge maps by name; default is nowobserver.paywallObserverkept as a deprecated aliassynchronize(success, error)now reports completion (was fire-and-forget)allowDeeplink/handleDeeplink(+ newallowCampaigns); oldreadyToOpenDeeplink/isDeeplinkHandledkept as deprecated aliasessetDefaultPresentationDismissHandler(old…ResultHandlerkept as alias)isFullscreenbool → display-mode string (bool still normalized); addbackPresentation(); removedpresentSubscriptions,presentProduct/PlanWithIdentifier,show/hidePresentation(no v6 primitive)onProcessAction(InterceptResult)replaces the bool (legacy bool mapped);PaywallActionkept +PresentationActionaliasInterceptResult,PresentationType,CloseReason,TransitionType,DimensionType,Store,StorekitVersion__tests__/Purchasely.test.js) updated in lockstep — 80 tests passiOS (
purchasely/src/ios/CDVPurchasely.m, Objective-C)@objcAPI (verified against the framework's generatedPurchasely-Swift.h) — no Swift rewrite/shim needed:apiKeybuilder →startWithInitialized:;PLYPresentationBuilder.forPlacementId:/forScreenId:→build→preloadWithCompletion:/displayFrom:transitionType:;interceptAction:handler:→PLYInterceptResult;setDefaultPresentationDismissHandler:;allowDeeplink:/handleDeeplink:/allowCampaigns:;closeAllScreens;backPresentationPLYPresentationOutcomeserialization (result int,closeReason,error,presentation)PLYAttributeis now an enum (was used as a pointer); declaredPLYEventDelegate/PLYUserAttributeDelegateconformance on the implementing categories; guarded user-attribute callbacks (v6 delegate setters are_Nonnull)Android (
purchasely/src/android/PurchaselyPlugin.kt, Kotlin)io.purchasely:core 6.0.0-rc.2: options-dictstart;PLYPresentationBase.builder().build().preload()→loaded.display(activity, transition){ outcome };interceptAction(…)→PLYInterceptResult; dismiss handler; deeplink renames;synchronizesuccess/error;CoroutineScopefor suspend queriesPLYProductActivity/PLYSubscriptionsActivity(+ layouts/themes,plugin.xmlsource-file/AndroidManifestentries) — dropped in native 6.0CI / E2E
ci.yml/publish.ymlaligned with Flutter (action bumps,workflow_dispatch, concurrency); prerelease tags publish under the npmnextdist-tag, stable underlateste2e-android.yml/e2e-ios.yml+ an Appium + WebdriverIO project (purchasely/example/e2e/) mirroring the FlutterE2E_TEST_INDEXsuite: a deterministic WEBVIEW-context bridge suite (hard gate) and a best-effort dismiss suite;workflow_dispatch+ nightly + scoped PRsDocs
MIGRATION-v6.md(new),VERSIONS.mdrow6.0.0-rc.1 → 6.0.0-rc.2 / 6.0.0-rc.2, READMEs,CLAUDE.md, example app updated to the 6.0 APIVerification
Purchasely 6.0.0-rc.2(BUILD SUCCEEDED; only a benignsetThemeModedeprecation)app-debug.apkagainstio.purchasely:core 6.0.0-rc.2, and runs on an API 34 emulator — SDK initializes (sdkVersion=6.0.0-rc.2), events flow (PRESENTATION_LOADED/VIEWED,PURCHASE_TAPPED), no crashNotes for reviewers
nextdist-tag forX.Y.Z-*tagsONBOARDING) must exist forcom.purchasely.demoon the example API key's backend — they need one real-runner shakeoutTODO(v6-verify)): drawer/popin transition dimensions default (JS sends only a display-mode string); a single interceptor completion is stashed per round-tripSee
VERSIONS.mdand the native release notes: iOS 6.0.0-rc.2, Android 6.0.0-rc.2.🤖 Generated with Claude Code