feat: migrate to Purchasely 6.0 native SDK (6.0.0-rc.1)#57
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>
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>
Greptile SummaryThis PR migrates the Cordova plugin to the Purchasely 6.0 native SDKs. The main changes are:
|
| Filename | Overview |
|---|---|
| purchasely/www/Purchasely.js | Updates the public JavaScript API for v6 startup options, display modes, deeplink methods, synchronization callbacks, and per-action interceptors. |
| purchasely/src/android/PurchaselyPlugin.kt | Migrates the Android bridge to v6 SDK calls, coroutine-based queries, loaded presentation display, and per-action interceptor completion. |
| purchasely/src/ios/CDVPurchasely.m | Migrates the iOS bridge to v6 builder, display, dismiss, deeplink, and interceptor APIs. |
| purchasely/plugin.xml | Updates native SDK versions and removes old Android presentation activity resources from plugin installation. |
| .github/workflows/ci.yml | Updates CI triggers, concurrency, Node version, and reusable action versions. |
| .github/workflows/e2e-android.yml | Adds Android Appium/WebdriverIO E2E coverage against an emulator and real backend. |
| .github/workflows/e2e-ios.yml | Adds iOS Appium/WebdriverIO E2E coverage against a simulator and real backend. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant App as Cordova app
participant JS as Purchasely.js
participant Native as Native bridge
participant SDK as Purchasely SDK
App->>JS: start(options)
JS->>Native: exec start
Native->>SDK: build/start v6 SDK
App->>JS: fetchPresentation(...)
JS->>Native: exec fetchPresentation
Native->>SDK: build + preload
SDK-->>Native: loaded presentation
Native-->>JS: presentation object
App->>JS: presentPresentation(presentation, mode)
JS->>Native: exec presentPresentation
Native->>SDK: display presentation
SDK-->>Native: outcome on dismiss/purchase
Native-->>JS: Cordova callback
%%{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 App as Cordova app
participant JS as Purchasely.js
participant Native as Native bridge
participant SDK as Purchasely SDK
App->>JS: start(options)
JS->>Native: exec start
Native->>SDK: build/start v6 SDK
App->>JS: fetchPresentation(...)
JS->>Native: exec fetchPresentation
Native->>SDK: build + preload
SDK-->>Native: loaded presentation
Native-->>JS: presentation object
App->>JS: presentPresentation(presentation, mode)
JS->>Native: exec presentPresentation
Native->>SDK: display presentation
SDK-->>Native: outcome on dismiss/purchase
Native-->>JS: Cordova callback
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
purchasely/src/android/PurchaselyPlugin.kt:570
**Synthetic Presentation Id Escapes**
When Android returns a fetched presentation, this replaces the native presentation id with a local `ply_fetch_...` handle. iOS returns the real id in the same `id` field, so app code that logs, stores, or passes `presentation.id` to another presentation API works on iOS but receives an Android-only handle that cannot identify the presentation.
### Issue 2 of 3
purchasely/src/ios/CDVPurchasely.m:1087
**Late Dismiss Handler Assignment**
For the `fetchPresentation` then `presentPresentation` flow, the dismiss handler is attached only after the presentation has already been built and preloaded. If the v6 presentation object captures lifecycle handlers during the builder/request phase, the paywall can display and dismiss without ever resolving the Cordova success callback.
### Issue 3 of 3
purchasely/src/android/PurchaselyPlugin.kt:594
**Shared Presentation Callback Slot**
Every Android presentation path writes to the same static `purchaseCallback`. If two presentations are started before the first outcome arrives, the later call overwrites the earlier callback, so the first caller never receives its dismiss or purchase result and the outcome can be delivered to the wrong Cordova command.
Reviews (1): Last reviewed commit: "refactor(api)!: remove deprecated v5 ali..." | Re-trigger Greptile
| val handle = "ply_fetch_${System.nanoTime()}" | ||
| loadedPresentations[handle] = loaded | ||
| val map = presentationToMap(loaded).toMutableMap() | ||
| map["id"] = handle |
There was a problem hiding this comment.
Synthetic Presentation Id Escapes
When Android returns a fetched presentation, this replaces the native presentation id with a local ply_fetch_... handle. iOS returns the real id in the same id field, so app code that logs, stores, or passes presentation.id to another presentation API works on iOS but receives an Android-only handle that cannot identify the presentation.
Context Used: CLAUDE.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: purchasely/src/android/PurchaselyPlugin.kt
Line: 570
Comment:
**Synthetic Presentation Id Escapes**
When Android returns a fetched presentation, this replaces the native presentation id with a local `ply_fetch_...` handle. iOS returns the real id in the same `id` field, so app code that logs, stores, or passes `presentation.id` to another presentation API works on iOS but receives an Android-only handle that cannot identify the presentation.
**Context Used:** CLAUDE.md ([source](https://app.greptile.com/purchasely/github/purchasely/purchasely-cordova/-/custom-context?memory=5b6f5482-f899-484f-8104-ac25efc0799e))
How can I resolve this? If you propose a fix, please make it concise.| presentationLoaded.controller.modalPresentationStyle = UIModalPresentationFullScreen; | ||
| } | ||
| // Deliver the dismiss outcome to this command through onDismissed. | ||
| presentationLoaded.onDismissed = ^(PLYPresentationOutcome * _Nonnull outcome) { |
There was a problem hiding this comment.
Late Dismiss Handler Assignment
For the fetchPresentation then presentPresentation flow, the dismiss handler is attached only after the presentation has already been built and preloaded. If the v6 presentation object captures lifecycle handlers during the builder/request phase, the paywall can display and dismiss without ever resolving the Cordova success callback.
Context Used: CLAUDE.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: purchasely/src/ios/CDVPurchasely.m
Line: 1087
Comment:
**Late Dismiss Handler Assignment**
For the `fetchPresentation` then `presentPresentation` flow, the dismiss handler is attached only after the presentation has already been built and preloaded. If the v6 presentation object captures lifecycle handlers during the builder/request phase, the paywall can display and dismiss without ever resolving the Cordova success callback.
**Context Used:** CLAUDE.md ([source](https://app.greptile.com/purchasely/github/purchasely/purchasely-cordova/-/custom-context?memory=5b6f5482-f899-484f-8104-ac25efc0799e))
How can I resolve this? If you propose a fix, please make it concise.| return | ||
| } | ||
|
|
||
| purchaseCallback = callbackContext |
There was a problem hiding this comment.
Shared Presentation Callback Slot
Every Android presentation path writes to the same static purchaseCallback. If two presentations are started before the first outcome arrives, the later call overwrites the earlier callback, so the first caller never receives its dismiss or purchase result and the outcome can be delivered to the wrong Cordova command.
Context Used: CLAUDE.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: purchasely/src/android/PurchaselyPlugin.kt
Line: 594
Comment:
**Shared Presentation Callback Slot**
Every Android presentation path writes to the same static `purchaseCallback`. If two presentations are started before the first outcome arrives, the later call overwrites the earlier callback, so the first caller never receives its dismiss or purchase result and the outcome can be delivered to the wrong Cordova command.
**Context Used:** CLAUDE.md ([source](https://app.greptile.com/purchasely/github/purchasely/purchasely-cordova/-/custom-context?memory=5b6f5482-f899-484f-8104-ac25efc0799e))
How can I resolve this? If you propose a fix, please make it concise.…backs, purchaseResult
Extend the Cordova presentation API to match Flutter PR #120 (verified against
head fdd5adbe). All additive / backward-compatible.
- Transition object on present*/displayMode: { type, dismissible, width, height:
{type,value}, backgroundColor } — a mode string and legacy boolean still work.
Native builds PLYDisplayMode (iOS) / PLYTransition (Android). iOS ObjC exposes
percentage height + dismissible + colors only (pixel sizing and popin width are
Swift-only in the SDK); Android honors the full set.
- Default (audience-targeted) presentation: presentPresentationForDefault() and
fetchPresentationForDefault() (native default builder — no placement/screen id).
- Per-request lifecycle: present* stream onPresented / onCloseRequested via an
optional final callbacks arg; the dismiss outcome stays the final success
callback (backward-compatible dispatcher). iOS covers direct + re-display;
Android covers the direct present* methods.
- removeDefaultPresentationDismissHandler().
- Dismiss outcome adds purchaseResult ('purchased'|'cancelled'|'restored')
alongside the legacy result int, matching the Flutter outcome contract.
Updates JS + iOS + Android bridges, unit tests (82 pass), MIGRATION-v6.md, and
the example app (exercises transitions, lifecycle callbacks, purchaseResult).
Both platforms build clean.
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