@purchasely/cordova-plugin-purchasely 5.7.2 → 6.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +12 -8
  2. package/__tests__/Purchasely.test.js +307 -92
  3. package/example/config.xml +0 -10
  4. package/example/e2e/README.md +44 -0
  5. package/example/e2e/helpers/driver.js +70 -0
  6. package/example/e2e/package.json +19 -0
  7. package/example/e2e/specs/bridge.e2e.js +51 -0
  8. package/example/e2e/specs/dismiss.e2e.js +37 -0
  9. package/example/e2e/tools/ci_run_e2e.sh +49 -0
  10. package/example/e2e/tools/ci_run_e2e_ios.sh +46 -0
  11. package/example/e2e/wdio.android.conf.js +21 -0
  12. package/example/e2e/wdio.ios.conf.js +19 -0
  13. package/example/e2e/wdio.shared.conf.js +28 -0
  14. package/example/package-lock.json +66 -209
  15. package/example/package.json +9 -5
  16. package/example/www/index.html +1 -7
  17. package/example/www/js/index.js +113 -92
  18. package/package.json +1 -1
  19. package/plugin.xml +3 -22
  20. package/src/android/PurchaselyPlugin.kt +529 -440
  21. package/src/ios/CDVPurchasely+Events.h +1 -1
  22. package/src/ios/CDVPurchasely+UserAttributes.h +1 -1
  23. package/src/ios/CDVPurchasely+UserAttributes.m +2 -0
  24. package/src/ios/CDVPurchasely.h +21 -16
  25. package/src/ios/CDVPurchasely.m +493 -257
  26. package/www/Purchasely.js +208 -40
  27. package/src/android/PLYProductActivity.kt +0 -168
  28. package/src/android/PLYSubscriptionsActivity.java +0 -37
  29. package/src/android/activity_ply_product_activity.xml +0 -6
  30. package/src/android/activity_ply_subscriptions_activity.xml +0 -6
  31. package/src/android/theme_purchasely_fullscreen.xml +0 -6
  32. package/src/android/theme_purchasely_fullscreen_v23.xml +0 -6
  33. package/src/android/theme_purchasely_fullscreen_v23_light_status_bar.xml +0 -7
@@ -0,0 +1,44 @@
1
+ # Purchasely Cordova — End-to-End tests
2
+
3
+ Device-level E2E tests for the example app, driving the **real** Purchasely 6.0 native
4
+ SDK through the Cordova bridge. Built with **Appium + WebdriverIO**:
5
+
6
+ - **WEBVIEW context** — calls `window.Purchasely.*` and asserts the results directly
7
+ (the deterministic `bridge` suite).
8
+ - **NATIVE_APP context** — injects OS-level touches (interceptor / dismiss suites).
9
+
10
+ These mirror the Flutter `integration_test` suite (`E2E_TEST_INDEX.md`) adapted to the
11
+ Cordova imperative API. They are **not** part of the PR-gating `ci.yml`; they run via the
12
+ `E2E Android` / `E2E iOS` workflows (`workflow_dispatch`, nightly `schedule`, and scoped
13
+ `pull_request`).
14
+
15
+ ## Suites & gating
16
+
17
+ | Suite | File | Gate | Notes |
18
+ |-------|------|------|-------|
19
+ | bridge | `specs/bridge.e2e.js` | **hard** | anonymous id, allProducts, fetchPresentationForPlacement, synchronize completion, user-attribute round-trip |
20
+ | dismiss | `specs/dismiss.e2e.js` | best-effort | present placement + programmatic close → dismiss outcome + `closeReason` (needs a paywall to render) |
21
+
22
+ Best-effort suites emit `::warning::` on failure and do not fail the job (native/paywall
23
+ rendering is flaky in CI — same policy as the Flutter suite). Each suite retries up to 3×.
24
+
25
+ ## Requirements to go green
26
+
27
+ 1. The placement `PURCHASELY_E2E_PLACEMENT` (default `ONBOARDING`) must exist on the
28
+ backend the example API key (`www/js/index.js`) points to, for app id
29
+ `com.purchasely.demo`. Override with the env var if your integration placement differs.
30
+ 2. Android: an emulator with Google APIs (the workflow uses API 34, `pixel_6`).
31
+ 3. iOS: an iPhone 16/15 simulator (the workflow discovers one).
32
+
33
+ ## Run locally
34
+
35
+ ```bash
36
+ # Android (emulator already booted, apk already built via `cordova build android`)
37
+ cd purchasely/example/e2e && npm install
38
+ bash ./tools/ci_run_e2e.sh emulator-5554
39
+
40
+ # iOS (simulator booted, app built via `cordova build ios --emulator`)
41
+ bash ./tools/ci_run_e2e_ios.sh <simulator-udid>
42
+ ```
43
+
44
+ Logs are written to `purchasely/example/e2e/ci-logs/` (uploaded as CI artifacts).
@@ -0,0 +1,70 @@
1
+ // Helpers for driving the Purchasely Cordova example app under Appium/WebdriverIO.
2
+ //
3
+ // Two contexts are used:
4
+ // * WEBVIEW — run JS against `window.Purchasely` (the cordova.exec bridge) and read
5
+ // results directly; used for the deterministic "bridge" assertions.
6
+ // * NATIVE_APP — inject OS-level touches (tap purchase button, press back) for the
7
+ // interceptor / dismiss suites.
8
+
9
+ async function switchToWebview() {
10
+ await browser.waitUntil(async () => {
11
+ const contexts = await browser.getContexts();
12
+ return contexts.some((c) => (typeof c === 'string' ? c : c.id).includes('WEBVIEW'));
13
+ }, { timeout: 60000, timeoutMsg: 'WEBVIEW context never appeared' });
14
+
15
+ const contexts = await browser.getContexts();
16
+ const webview = contexts
17
+ .map((c) => (typeof c === 'string' ? c : c.id))
18
+ .find((id) => id.includes('WEBVIEW'));
19
+ await browser.switchContext(webview);
20
+ }
21
+
22
+ async function switchToNative() {
23
+ await browser.switchContext('NATIVE_APP');
24
+ }
25
+
26
+ // Wait until the Purchasely bridge is available (deviceready fired + plugin clobbered).
27
+ async function waitForPurchaselyReady() {
28
+ await switchToWebview();
29
+ await browser.waitUntil(
30
+ async () => browser.execute(() => typeof window.Purchasely !== 'undefined'),
31
+ { timeout: 60000, timeoutMsg: 'window.Purchasely never became available' }
32
+ );
33
+ }
34
+
35
+ // Invoke a Purchasely method that takes trailing (success, error) callbacks and resolve
36
+ // with { ok, value } on success or { ok:false, error } on error. `args` are the leading
37
+ // positional arguments before the callbacks.
38
+ async function callBridge(method, args = [], timeoutMs = 30000) {
39
+ return browser.executeAsync(
40
+ function (method, args, timeoutMs, done) {
41
+ var settled = false;
42
+ var finish = function (payload) {
43
+ if (settled) return;
44
+ settled = true;
45
+ done(payload);
46
+ };
47
+ setTimeout(function () { finish({ ok: false, error: 'timeout' }); }, timeoutMs);
48
+ try {
49
+ var fn = window.Purchasely[method];
50
+ if (typeof fn !== 'function') {
51
+ return finish({ ok: false, error: 'no such method: ' + method });
52
+ }
53
+ fn.apply(
54
+ window.Purchasely,
55
+ args.concat([
56
+ function (value) { finish({ ok: true, value: value }); },
57
+ function (error) { finish({ ok: false, error: String(error) }); },
58
+ ])
59
+ );
60
+ } catch (e) {
61
+ finish({ ok: false, error: String(e) });
62
+ }
63
+ },
64
+ method,
65
+ args,
66
+ timeoutMs
67
+ );
68
+ }
69
+
70
+ module.exports = { switchToWebview, switchToNative, waitForPurchaselyReady, callBridge };
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "purchasely-cordova-e2e",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "End-to-end tests for the Purchasely Cordova example app (Appium + WebdriverIO). Drives the real native Purchasely 6.0 SDK inside the WebView (bridge assertions) and injects OS-level touches (interceptor / dismiss).",
6
+ "scripts": {
7
+ "test:android": "wdio run ./wdio.android.conf.js",
8
+ "test:ios": "wdio run ./wdio.ios.conf.js"
9
+ },
10
+ "devDependencies": {
11
+ "@wdio/cli": "^9.0.0",
12
+ "@wdio/local-runner": "^9.0.0",
13
+ "@wdio/mocha-framework": "^9.0.0",
14
+ "@wdio/spec-reporter": "^9.0.0",
15
+ "appium": "^2.11.0",
16
+ "appium-uiautomator2-driver": "^3.9.0",
17
+ "appium-xcuitest-driver": "^7.28.0"
18
+ }
19
+ }
@@ -0,0 +1,51 @@
1
+ // Deterministic Dart<->native bridge assertions (no native taps). HARD gate — these
2
+ // must pass. Mirrors the Flutter E2E_TEST_INDEX suite T1/T3/T5/T6 adapted to the
3
+ // Cordova imperative API.
4
+ const { waitForPurchaselyReady, callBridge } = require('../helpers/driver');
5
+
6
+ const PLACEMENT = process.env.PURCHASELY_E2E_PLACEMENT || 'ONBOARDING';
7
+
8
+ describe('Purchasely bridge (WEBVIEW context)', () => {
9
+ before(async () => {
10
+ await waitForPurchaselyReady();
11
+ });
12
+
13
+ // T1 — anonymous user id
14
+ it('getAnonymousUserId returns a non-empty id', async () => {
15
+ const res = await callBridge('getAnonymousUserId');
16
+ expect(res.ok).toBe(true);
17
+ expect(typeof res.value).toBe('string');
18
+ expect(res.value.length).toBeGreaterThan(0);
19
+ });
20
+
21
+ // T5 — catalog
22
+ it('allProducts returns a list', async () => {
23
+ const res = await callBridge('allProducts');
24
+ expect(res.ok).toBe(true);
25
+ expect(Array.isArray(res.value)).toBe(true);
26
+ });
27
+
28
+ // T3 — preload a presentation for a placement
29
+ it('fetchPresentationForPlacement returns a presentation object', async () => {
30
+ const res = await callBridge('fetchPresentationForPlacement', [PLACEMENT, null]);
31
+ expect(res.ok).toBe(true);
32
+ expect(res.value).toBeDefined();
33
+ // v6 presentation carries an id/type so it can later be displayed.
34
+ expect(res.value === null || typeof res.value === 'object').toBe(true);
35
+ });
36
+
37
+ // T6 — synchronize now reports completion (v6 change). On a bare emulator/simulator
38
+ // with no billing, exactly one of success/error must fire (no fire-and-forget).
39
+ it('synchronize resolves exactly one callback', async () => {
40
+ const res = await callBridge('synchronize');
41
+ expect(typeof res.ok).toBe('boolean');
42
+ });
43
+
44
+ // user-attribute round-trip (set then read back)
45
+ it('setUserAttributeWithString then userAttribute round-trips', async () => {
46
+ await callBridge('setUserAttributeWithString', ['e2e_key', 'e2e_value', 'ESSENTIAL']);
47
+ const res = await callBridge('userAttribute', ['e2e_key']);
48
+ expect(res.ok).toBe(true);
49
+ expect(res.value).toBe('e2e_value');
50
+ });
51
+ });
@@ -0,0 +1,37 @@
1
+ // Presentation display + dismiss outcome. BEST-EFFORT (non-blocking in CI): depends on
2
+ // a paywall actually rendering for the configured placement against the real backend.
3
+ // Mirrors E2E_TEST_INDEX T8/T12 adapted to the Cordova imperative API.
4
+ const { waitForPurchaselyReady, callBridge, switchToNative } = require('../helpers/driver');
5
+
6
+ const PLACEMENT = process.env.PURCHASELY_E2E_PLACEMENT || 'ONBOARDING';
7
+
8
+ describe('Presentation dismiss outcome', () => {
9
+ before(async () => {
10
+ await waitForPurchaselyReady();
11
+ });
12
+
13
+ // T8/T12 — the present* success callback IS the per-presentation dismiss outcome.
14
+ // Present a placement, close it programmatically, and assert the outcome fires with a
15
+ // closeReason.
16
+ it('presentPresentationForPlacement + closePresentation delivers a dismiss outcome', async () => {
17
+ // Kick off the presentation; do NOT await (the callback resolves at dismiss).
18
+ const outcomePromise = callBridge(
19
+ 'presentPresentationForPlacement',
20
+ [PLACEMENT, null, 'fullScreen'],
21
+ 90000
22
+ );
23
+
24
+ // Give the paywall time to render, then close it programmatically from the bridge.
25
+ await browser.pause(6000);
26
+ await callBridge('closePresentation');
27
+
28
+ const outcome = await outcomePromise;
29
+ expect(outcome.ok).toBe(true);
30
+ expect(outcome.value).toBeDefined();
31
+ // v6 outcome carries a closeReason; programmatic close => 'programmatic' (Android).
32
+ if (outcome.value && outcome.value.closeReason) {
33
+ expect(typeof outcome.value.closeReason).toBe('string');
34
+ }
35
+ await switchToNative().catch(() => {});
36
+ });
37
+ });
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # Android E2E runner. Assumes:
3
+ # * an emulator is already booted (serial passed as $1, e.g. emulator-5554)
4
+ # * the debug apk has been built (cordova build android)
5
+ # * node deps for purchasely/example/e2e are installed
6
+ #
7
+ # Gating mirrors the Flutter suite: the deterministic `bridge` suite HARD-gates the job;
8
+ # `dismiss` (needs a paywall to render against the real backend) is BEST-EFFORT and only
9
+ # emits ::warning:: on failure.
10
+ set -uo pipefail
11
+
12
+ SERIAL="${1:-emulator-5554}"
13
+ HERE="$(cd "$(dirname "$0")/.." && pwd)"
14
+ LOGDIR="$HERE/ci-logs"
15
+ mkdir -p "$LOGDIR"
16
+ export ANDROID_SERIAL="$SERIAL"
17
+
18
+ echo "== Starting Appium =="
19
+ npx appium --log "$LOGDIR/appium-android.log" --log-level info &
20
+ APPIUM_PID=$!
21
+ trap 'kill $APPIUM_PID 2>/dev/null || true' EXIT
22
+ # Wait for Appium to accept connections.
23
+ for i in $(seq 1 30); do
24
+ curl -sf http://127.0.0.1:4723/status >/dev/null 2>&1 && break
25
+ sleep 1
26
+ done
27
+
28
+ run_suite() { # $1 = spec glob, $2 = hard|soft
29
+ local spec="$1" gate="$2" tries=3 n=1
30
+ while [ $n -le $tries ]; do
31
+ echo "== [$gate] $spec (attempt $n/$tries) =="
32
+ if PURCHASELY_E2E_SPEC="$spec" npx wdio run ./wdio.android.conf.js --spec "$spec" 2>&1 | tee "$LOGDIR/wdio-$(basename "$spec").log"; then
33
+ return 0
34
+ fi
35
+ n=$((n+1))
36
+ done
37
+ if [ "$gate" = "hard" ]; then
38
+ echo "::error::E2E suite failed (hard gate): $spec"
39
+ return 1
40
+ fi
41
+ echo "::warning::E2E suite failed (best-effort): $spec"
42
+ return 0
43
+ }
44
+
45
+ cd "$HERE"
46
+ rc=0
47
+ run_suite "./specs/bridge.e2e.js" hard || rc=1
48
+ run_suite "./specs/dismiss.e2e.js" soft || true
49
+ exit $rc
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env bash
2
+ # iOS E2E runner. Assumes:
3
+ # * a simulator is already booted (udid passed as $1)
4
+ # * the simulator .app has been built (cordova build ios --emulator)
5
+ # * node deps for purchasely/example/e2e are installed
6
+ #
7
+ # Same gating as Android: `bridge` HARD-gates; `dismiss` is BEST-EFFORT.
8
+ set -uo pipefail
9
+
10
+ UDID="${1:-booted}"
11
+ HERE="$(cd "$(dirname "$0")/.." && pwd)"
12
+ LOGDIR="$HERE/ci-logs"
13
+ mkdir -p "$LOGDIR"
14
+ export PURCHASELY_E2E_UDID="$UDID"
15
+
16
+ echo "== Starting Appium =="
17
+ npx appium --log "$LOGDIR/appium-ios.log" --log-level info &
18
+ APPIUM_PID=$!
19
+ trap 'kill $APPIUM_PID 2>/dev/null || true' EXIT
20
+ for i in $(seq 1 30); do
21
+ curl -sf http://127.0.0.1:4723/status >/dev/null 2>&1 && break
22
+ sleep 1
23
+ done
24
+
25
+ run_suite() { # $1 = spec, $2 = hard|soft
26
+ local spec="$1" gate="$2" tries=3 n=1
27
+ while [ $n -le $tries ]; do
28
+ echo "== [$gate] $spec (attempt $n/$tries) =="
29
+ if npx wdio run ./wdio.ios.conf.js --spec "$spec" 2>&1 | tee "$LOGDIR/wdio-$(basename "$spec").log"; then
30
+ return 0
31
+ fi
32
+ n=$((n+1))
33
+ done
34
+ if [ "$gate" = "hard" ]; then
35
+ echo "::error::E2E suite failed (hard gate): $spec"
36
+ return 1
37
+ fi
38
+ echo "::warning::E2E suite failed (best-effort): $spec"
39
+ return 0
40
+ }
41
+
42
+ cd "$HERE"
43
+ rc=0
44
+ run_suite "./specs/bridge.e2e.js" hard || rc=1
45
+ run_suite "./specs/dismiss.e2e.js" soft || true
46
+ exit $rc
@@ -0,0 +1,21 @@
1
+ const path = require('path');
2
+ const { config } = require('./wdio.shared.conf');
3
+
4
+ // Path to the debug apk built by `cordova build android`.
5
+ const APK = process.env.PURCHASELY_E2E_APK ||
6
+ path.resolve(__dirname, '../platforms/android/app/build/outputs/apk/debug/app-debug.apk');
7
+
8
+ exports.config = Object.assign({}, config, {
9
+ capabilities: [{
10
+ platformName: 'Android',
11
+ 'appium:automationName': 'UiAutomator2',
12
+ 'appium:app': APK,
13
+ 'appium:appPackage': 'com.purchasely.demo',
14
+ 'appium:newCommandTimeout': 240,
15
+ 'appium:autoGrantPermissions': true,
16
+ // The Cordova WebView is debuggable in the debug build, so Appium can attach
17
+ // chromedriver and expose the WEBVIEW context.
18
+ 'appium:ensureWebviewsHavePages': true,
19
+ 'appium:nativeWebScreenshot': true,
20
+ }],
21
+ });
@@ -0,0 +1,19 @@
1
+ const path = require('path');
2
+ const { config } = require('./wdio.shared.conf');
3
+
4
+ // Path to the .app built by `cordova build ios --emulator` (simulator build).
5
+ const APP = process.env.PURCHASELY_E2E_APP ||
6
+ path.resolve(__dirname, '../platforms/ios/build/emulator/HelloCordova.app');
7
+
8
+ exports.config = Object.assign({}, config, {
9
+ capabilities: [{
10
+ platformName: 'iOS',
11
+ 'appium:automationName': 'XCUITest',
12
+ 'appium:app': APP,
13
+ 'appium:bundleId': 'com.purchasely.demo',
14
+ 'appium:deviceName': process.env.PURCHASELY_E2E_SIM || 'iPhone 16',
15
+ 'appium:platformVersion': process.env.PURCHASELY_E2E_IOS_VERSION || undefined,
16
+ 'appium:newCommandTimeout': 240,
17
+ 'appium:autoAcceptAlerts': true,
18
+ }],
19
+ });
@@ -0,0 +1,28 @@
1
+ // Shared WebdriverIO config for the Purchasely Cordova E2E suite.
2
+ // Platform-specific configs (wdio.android.conf.js / wdio.ios.conf.js) extend this
3
+ // and set `capabilities`.
4
+ //
5
+ // The example app boots the Purchasely SDK on `deviceready` (see www/js/index.js).
6
+ // Tests switch to the WEBVIEW context and call `window.Purchasely.*` directly, and
7
+ // switch to NATIVE_APP to inject OS-level touches for the interceptor / dismiss suites.
8
+ exports.config = {
9
+ runner: 'local',
10
+ specs: ['./specs/**/*.e2e.js'],
11
+ maxInstances: 1,
12
+ logLevel: 'info',
13
+ bail: 0,
14
+ waitforTimeout: 20000,
15
+ connectionRetryTimeout: 120000,
16
+ connectionRetryCount: 2,
17
+ framework: 'mocha',
18
+ reporters: ['spec'],
19
+ mochaOpts: {
20
+ ui: 'bdd',
21
+ timeout: 120000,
22
+ },
23
+ // Appium is started as a service by the CI runner scripts (tools/ci_run_e2e*.sh),
24
+ // so we point WDIO at the already-running server rather than the @wdio/appium-service.
25
+ hostname: '127.0.0.1',
26
+ port: 4723,
27
+ path: '/',
28
+ };