@purchasely/cordova-plugin-purchasely 6.0.0 → 6.1.0

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.
@@ -1,25 +0,0 @@
1
- # cordova-ios 8.1.1 Manual Test Matrix
2
-
3
- Run this matrix on a physical iOS device or a StoreKit-configured simulator after:
4
-
5
- ```bash
6
- cd purchasely/example
7
- ./ios.sh
8
- cordova run ios --device
9
- ```
10
-
11
- Use a Purchasely project with a presentation, a purchasable product, and a restorable
12
- purchase. Capture the app logs and the callback payloads for every row.
13
-
14
- | Scenario | Steps | Expected result |
15
- | --- | --- | --- |
16
- | Presentation | Trigger the sample presentation. | The configured presentation is displayed using the requested transition. |
17
- | Purchase | Select a product and complete the StoreKit purchase. | The StoreKit sheet completes, the purchase callback receives the Purchasely outcome, and the active entitlement is updated. |
18
- | Restore | Use the sample restore action with a previously purchased account. | The restore callback completes and the restored entitlement is available. |
19
- | Close | Dismiss the presentation through its close control and, separately, through the system back/dismiss gesture when enabled. | The close callback fires once per dismissal with the matching close reason; the final presentation outcome is delivered. |
20
- | JavaScript callbacks | Repeat presentation, purchase, restore, and close while logging every success, failure, presented, and close-requested callback. | Each callback is received on the JavaScript bridge with its expected payload and no duplicate terminal callback. |
21
- | Background and foreground | Show a presentation or StoreKit sheet, send the app to the background, then return it to the foreground. | The app remains responsive, the visible flow resumes or completes according to StoreKit, and no callback is lost or duplicated. |
22
-
23
- This matrix is runtime validation only. The CI job verifies deterministic installation,
24
- CocoaPods workspace build, and an unsigned archive; it cannot validate StoreKit or
25
- backend-configured presentation behavior.
@@ -1,10 +0,0 @@
1
- #!/bin/bash
2
-
3
- cordova plugin remove @purchasely/cordova-plugin-purchasely
4
- cordova plugin remove @purchasely/cordova-plugin-purchasely-google
5
- cordova plugin remove cordova-plugin-purchasely
6
- cordova plugin remove cordova-plugin-purchasely-google
7
- cordova platform remove android
8
- cordova platform add android@latest
9
- cordova plugin add ../ --link
10
- cordova plugin add ../../purchasely-google/ --link
@@ -1,27 +0,0 @@
1
- <?xml version='1.0' encoding='utf-8'?>
2
- <widget id="com.purchasely.demo" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
3
- <name>HelloCordova</name>
4
- <description>
5
- A sample Apache Cordova application that responds to the deviceready event.
6
- </description>
7
- <author email="dev@cordova.apache.org" href="http://cordova.io">
8
- Apache Cordova Team
9
- </author>
10
- <content src="index.html" />
11
- <access origin="*" />
12
- <allow-intent href="http://*/*" />
13
- <allow-intent href="https://*/*" />
14
- <allow-intent href="tel:*" />
15
- <allow-intent href="sms:*" />
16
- <allow-intent href="mailto:*" />
17
- <allow-intent href="geo:*" />
18
- <platform name="android">
19
- <allow-intent href="market:*" />
20
- <preference name="GradlePluginKotlinEnabled" value="true" />
21
- <preference name="GradlePluginKotlinVersion" value="2.3.21" />
22
- </platform>
23
- <platform name="ios">
24
- <allow-intent href="itms:*" />
25
- <allow-intent href="itms-apps:*" />
26
- </platform>
27
- </widget>
@@ -1,44 +0,0 @@
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 (string/int/boolean), userSubscriptions |
20
- | dismiss | `specs/dismiss.e2e.js` | best-effort | present placement or default presentation + 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).
@@ -1,187 +0,0 @@
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
- // Poll a window global until a native callback has populated it, then return it.
36
- // Resolves { ok:false, error:'timeout' } if nothing arrives in time. We poll a SYNC
37
- // execute rather than use executeAsync because the iOS WKWebView aborts async-script
38
- // results almost immediately ("Timed out waiting for asynchronous script result"),
39
- // which would fail every bridge call on iOS.
40
- async function pollGlobal(name, timeoutMs) {
41
- let value;
42
- try {
43
- await browser.waitUntil(
44
- async () => {
45
- value = await browser.execute(function (n) { return window[n]; }, name);
46
- return value !== undefined && value !== null;
47
- },
48
- { timeout: timeoutMs, interval: 250, timeoutMsg: name + ' never settled' }
49
- );
50
- } catch (e) {
51
- return { ok: false, error: 'timeout' };
52
- }
53
- return value;
54
- }
55
-
56
- // Invoke a Purchasely method that takes trailing (success, error) callbacks and resolve
57
- // with { ok, value } on success or { ok:false, error } on error. `args` are the leading
58
- // positional arguments before the callbacks. Fires via a sync execute that stashes the
59
- // settled result on window.__plyResult, then polls for it (see pollGlobal).
60
- async function callBridge(method, args = [], timeoutMs = 30000) {
61
- await browser.execute(
62
- function (method, args) {
63
- window.__plyResult = undefined;
64
- var settled = false;
65
- var finish = function (payload) { if (!settled) { settled = true; window.__plyResult = payload; } };
66
- try {
67
- var fn = window.Purchasely[method];
68
- if (typeof fn !== 'function') { finish({ ok: false, error: 'no such method: ' + method }); return; }
69
- fn.apply(
70
- window.Purchasely,
71
- args.concat([
72
- function (value) { finish({ ok: true, value: value }); },
73
- function (error) { finish({ ok: false, error: String(error) }); },
74
- ])
75
- );
76
- } catch (e) { finish({ ok: false, error: String(e) }); }
77
- },
78
- method,
79
- args
80
- );
81
- return pollGlobal('__plyResult', timeoutMs);
82
- }
83
-
84
- // Fire a void Purchasely method that takes NO success callback — the
85
- // setUserAttributeWith* setters are fire-and-forget on the Cordova bridge
86
- // (exec(() => {}, defaultError, ...)). callBridge would hang waiting for a success
87
- // callback these never invoke, so dispatch the call and resolve immediately.
88
- async function fireBridge(method, args = []) {
89
- await browser.execute(
90
- function (method, args) {
91
- var fn = window.Purchasely[method];
92
- if (typeof fn === 'function') fn.apply(window.Purchasely, args);
93
- },
94
- method,
95
- args
96
- );
97
- }
98
-
99
- // Drive the v6 presentation builder (Purchasely.presentation) from WEBVIEW context and
100
- // resolve with { ok, value } / { ok:false, error } -- same contract as callBridge, since
101
- // fetchPresentation*/presentPresentation* (and their (args..., success, error) shape that
102
- // callBridge drives) were removed in favor of the promise-based builder.
103
- // `source` is 'placement' | 'screen' | 'defaultSource'; `sourceId` is the placement/screen
104
- // id (omit for 'defaultSource'); `action` is 'preload' or 'display'; `transition` is passed
105
- // to display() when action is 'display'. Stashes the built request on
106
- // `window.__plyLastRequest` so a follow-up closeCurrentPresentation()/backCurrentPresentation()
107
- // call in the same test can drive it (mirrors closePresentation()/backPresentation() acting
108
- // on the natively-tracked current presentation pre-builder).
109
- async function callPresentation(source, sourceId, action, transition, timeoutMs = 90000) {
110
- await browser.execute(
111
- function (source, sourceId, action, transition) {
112
- window.__plyResult = undefined;
113
- var settled = false;
114
- var finish = function (payload) { if (!settled) { settled = true; window.__plyResult = payload; } };
115
- try {
116
- var builder = sourceId
117
- ? window.Purchasely.presentation[source](sourceId)
118
- : window.Purchasely.presentation[source]();
119
- var request = builder.build();
120
- window.__plyLastRequest = request;
121
- var promise = action === 'preload' ? request.preload() : request.display(transition);
122
- promise.then(
123
- function (value) { finish({ ok: true, value: value }); },
124
- function (error) { finish({ ok: false, error: String(error) }); }
125
- );
126
- } catch (e) { finish({ ok: false, error: String(e) }); }
127
- },
128
- source,
129
- sourceId,
130
- action,
131
- transition
132
- );
133
- return pollGlobal('__plyResult', timeoutMs);
134
- }
135
-
136
- // Display a presentation and stash its dismiss outcome on a window global, WITHOUT
137
- // blocking the WebDriver session. callPresentation() drives display() inside a single
138
- // executeAsync that only settles at dismiss — but the session is then busy, so the
139
- // follow-up close() command can never run (deadlock -> script timeout). Fire display()
140
- // fire-and-forget instead and poll the outcome via awaitDismissOutcome(); the session
141
- // stays free to send closeCurrentPresentation() in between.
142
- async function displayPresentation(source, sourceId, transition) {
143
- await browser.execute(
144
- function (source, sourceId, transition) {
145
- window.__plyOutcome = undefined;
146
- var builder = sourceId
147
- ? window.Purchasely.presentation[source](sourceId)
148
- : window.Purchasely.presentation[source]();
149
- var request = builder.build();
150
- window.__plyLastRequest = request;
151
- request.display(transition).then(
152
- function (v) { window.__plyOutcome = { ok: true, value: v }; },
153
- function (e) { window.__plyOutcome = { ok: false, error: String(e) }; }
154
- );
155
- },
156
- source,
157
- sourceId,
158
- transition
159
- );
160
- }
161
-
162
- // Poll for the dismiss outcome stashed by displayPresentation(). Resolves { ok, value } /
163
- // { ok:false, error } once display()'s promise settles (i.e. after close()), or
164
- // { ok:false, error:'timeout' } if no outcome arrives in time.
165
- async function awaitDismissOutcome(timeoutMs = 30000) {
166
- return pollGlobal('__plyOutcome', timeoutMs);
167
- }
168
-
169
- // Close the presentation driven by the last callPresentation()/displayPresentation()
170
- // request (WEBVIEW context).
171
- async function closeCurrentPresentation() {
172
- return browser.execute(function () {
173
- if (window.__plyLastRequest) window.__plyLastRequest.close();
174
- });
175
- }
176
-
177
- module.exports = {
178
- switchToWebview,
179
- switchToNative,
180
- waitForPurchaselyReady,
181
- callBridge,
182
- fireBridge,
183
- callPresentation,
184
- displayPresentation,
185
- awaitDismissOutcome,
186
- closeCurrentPresentation,
187
- };
@@ -1,19 +0,0 @@
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
- }
@@ -1,154 +0,0 @@
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, fireBridge, callPresentation } = 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. Store-dependent: the iOS simulator has no StoreKit products configured,
22
- // so allProducts settles as a clean error there while the Android emulator (Google
23
- // Billing) returns a list. Assert the bridge round-trips cleanly either way.
24
- it('allProducts returns a list', async () => {
25
- const res = await callBridge('allProducts');
26
- if (res.ok) {
27
- expect(Array.isArray(res.value)).toBe(true);
28
- } else {
29
- expect(typeof res.error).toBe('string');
30
- }
31
- });
32
-
33
- // T3 — preload a presentation for a placement (was fetchPresentationForPlacement;
34
- // now Purchasely.presentation.placement(id).build().preload())
35
- it('presentation.placement(...).build().preload() returns a presentation object', async () => {
36
- const res = await callPresentation('placement', PLACEMENT, 'preload');
37
- // Store-dependent (preload fetches the paywall + its products): tolerate a clean error
38
- // on the store-less iOS simulator, assert the shape when it resolves.
39
- if (res.ok) {
40
- // v6 presentation normalizes screenId as the authoritative identifier.
41
- expect(res.value === null || typeof res.value === 'object').toBe(true);
42
- if (res.value) {
43
- expect(typeof res.value.screenId).toBe('string');
44
- }
45
- } else {
46
- expect(typeof res.error).toBe('string');
47
- }
48
- });
49
-
50
- // T6 — synchronize now reports completion (v6 change). On a bare emulator/simulator
51
- // with no billing, exactly one of success/error must fire (no fire-and-forget).
52
- it('synchronize resolves exactly one callback', async () => {
53
- const res = await callBridge('synchronize');
54
- expect(typeof res.ok).toBe('boolean');
55
- });
56
-
57
- // user-attribute round-trip (set then read back). The setters are fire-and-forget on
58
- // the Cordova bridge (no success callback), so fire them via fireBridge and read back
59
- // with callBridge; the small pause lets the native set land before the read.
60
- it('setUserAttributeWithString then userAttribute round-trips', async () => {
61
- await fireBridge('setUserAttributeWithString', ['e2e_key', 'e2e_value', 'ESSENTIAL']);
62
- await browser.pause(300);
63
- const res = await callBridge('userAttribute', ['e2e_key']);
64
- expect(res.ok).toBe(true);
65
- expect(res.value).toBe('e2e_value');
66
- });
67
-
68
- // user-attribute round-trip, int variant
69
- it('setUserAttributeWithInt then userAttribute round-trips', async () => {
70
- await fireBridge('setUserAttributeWithInt', ['e2e_key_int', 7, 'ESSENTIAL']);
71
- await browser.pause(300);
72
- const res = await callBridge('userAttribute', ['e2e_key_int']);
73
- expect(res.ok).toBe(true);
74
- expect(res.value).toBe(7);
75
- });
76
-
77
- // user-attribute round-trip, boolean variant (CDV-W-09 fixed: both platforms now
78
- // return a real JSON boolean, so this asserts strict equality, not just truthiness).
79
- it('setUserAttributeWithBoolean then userAttribute round-trips', async () => {
80
- await fireBridge('setUserAttributeWithBoolean', ['e2e_key_bool', true, 'ESSENTIAL']);
81
- await browser.pause(300);
82
- const res = await callBridge('userAttribute', ['e2e_key_bool']);
83
- expect(res.ok).toBe(true);
84
- expect(res.value).toBe(true);
85
- });
86
-
87
- // userSubscriptions on a fresh anonymous user. Store-dependent: settles as a list on the
88
- // Android emulator, can settle as a clean error on the store-less iOS simulator.
89
- it('userSubscriptions returns a list', async () => {
90
- const res = await callBridge('userSubscriptions');
91
- if (res.ok) {
92
- expect(Array.isArray(res.value)).toBe(true);
93
- } else {
94
- expect(typeof res.error).toBe('string');
95
- }
96
- });
97
-
98
- // T2 — login/logout cycle: isAnonymous flips true -> false -> true. userLogin/userLogout
99
- // are fire-and-forget on the Cordova bridge, so drive them via fireBridge + verify via
100
- // the callback-based isAnonymous getter.
101
- it('isAnonymous flips around userLogin / userLogout', async () => {
102
- let res = await callBridge('isAnonymous');
103
- expect(res.ok).toBe(true);
104
- expect(res.value).toBe(true);
105
-
106
- await fireBridge('userLogin', ['cordova_e2e_user']);
107
- await browser.pause(500);
108
- res = await callBridge('isAnonymous');
109
- expect(res.value).toBe(false);
110
-
111
- await fireBridge('userLogout', []);
112
- await browser.pause(500);
113
- res = await callBridge('isAnonymous');
114
- expect(res.value).toBe(true);
115
- });
116
-
117
- // T4 — dynamic offerings list (may be empty on a bare emulator)
118
- it('getDynamicOfferings returns a list', async () => {
119
- const res = await callBridge('getDynamicOfferings');
120
- expect(res.ok).toBe(true);
121
- expect(Array.isArray(res.value)).toBe(true);
122
- });
123
-
124
- // T18 — dynamic offerings set / get / remove / clear round-trip (list stays an array,
125
- // no throw). The mutators are fire-and-forget; getDynamicOfferings reads back.
126
- it('dynamic offerings set / remove / clear round-trip', async () => {
127
- await fireBridge('setDynamicOffering', ['e2e_ref', 'e2e_plan', null, 0]);
128
- await browser.pause(300);
129
- let res = await callBridge('getDynamicOfferings');
130
- expect(Array.isArray(res.value)).toBe(true);
131
-
132
- await fireBridge('removeDynamicOffering', ['e2e_ref']);
133
- await fireBridge('clearDynamicOfferings', []);
134
- await browser.pause(300);
135
- res = await callBridge('getDynamicOfferings');
136
- expect(res.ok).toBe(true);
137
- expect(Array.isArray(res.value)).toBe(true);
138
- });
139
-
140
- // T16 — increment / decrement a numeric user attribute
141
- it('increment / decrement a numeric user attribute', async () => {
142
- await fireBridge('setUserAttributeWithInt', ['e2e_counter', 5, 'ESSENTIAL']);
143
- await browser.pause(300);
144
- await fireBridge('incrementUserAttribute', ['e2e_counter', 3]);
145
- await browser.pause(300);
146
- let res = await callBridge('userAttribute', ['e2e_counter']);
147
- expect(res.value).toBe(8);
148
-
149
- await fireBridge('decrementUserAttribute', ['e2e_counter', 2]);
150
- await browser.pause(300);
151
- res = await callBridge('userAttribute', ['e2e_counter']);
152
- expect(res.value).toBe(6);
153
- });
154
- });
@@ -1,57 +0,0 @@
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 {
5
- waitForPurchaselyReady,
6
- displayPresentation,
7
- awaitDismissOutcome,
8
- closeCurrentPresentation,
9
- } = require('../helpers/driver');
10
-
11
- const PLACEMENT = process.env.PURCHASELY_E2E_PLACEMENT || 'ONBOARDING';
12
-
13
- describe('Presentation dismiss outcome', () => {
14
- before(async () => {
15
- await waitForPurchaselyReady();
16
- });
17
-
18
- // T8/T12 — display() resolves with the per-presentation dismiss outcome (was the
19
- // presentPresentationForPlacement success callback; closePresentation() is now
20
- // request.close(), driven here via closeCurrentPresentation()).
21
- // Present a placement, close it programmatically, and assert the outcome fires with a
22
- // closeReason.
23
- it('presentation.placement(...).build().display() + request.close() delivers a dismiss outcome', async () => {
24
- // Fire display() fire-and-forget (its promise settles at dismiss and would otherwise
25
- // block the session so close() could never run).
26
- await displayPresentation('placement', PLACEMENT, 'fullScreen');
27
-
28
- // Give the paywall time to render, then close it programmatically from the bridge.
29
- await browser.pause(6000);
30
- await closeCurrentPresentation();
31
-
32
- const outcome = await awaitDismissOutcome(30000);
33
- expect(outcome.ok).toBe(true);
34
- expect(outcome.value).toBeDefined();
35
- // v6 outcome carries a closeReason; programmatic close => 'programmatic' (Android).
36
- if (outcome.value && outcome.value.closeReason) {
37
- expect(typeof outcome.value.closeReason).toBe('string');
38
- }
39
- });
40
-
41
- // v6 default (audience-targeted) presentation: same shape as the placement flow above,
42
- // just with no placement/screen id (was presentPresentationForDefault). Best-effort:
43
- // depends on a default audience being configured on the backend for this app id.
44
- it('presentation.defaultSource().build().display() + request.close() delivers a dismiss outcome', async () => {
45
- await displayPresentation('defaultSource', null, 'fullScreen');
46
-
47
- await browser.pause(6000);
48
- await closeCurrentPresentation();
49
-
50
- const outcome = await awaitDismissOutcome(30000);
51
- expect(outcome.ok).toBe(true);
52
- expect(outcome.value).toBeDefined();
53
- if (outcome.value && outcome.value.closeReason) {
54
- expect(typeof outcome.value.closeReason).toBe('string');
55
- }
56
- });
57
- });
@@ -1,63 +0,0 @@
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
- # Appium 2 loads drivers from APPIUM_HOME (~/.appium), not node_modules, so a fresh CI
19
- # checkout has none installed even though appium-uiautomator2-driver is a devDependency.
20
- # Register it (idempotent; a no-op locally where it is already installed).
21
- echo "== Ensuring uiautomator2 driver is installed =="
22
- npx appium driver install uiautomator2 2>/dev/null || true
23
-
24
- echo "== Starting Appium =="
25
- # --allow-insecure=chromedriver_autodownload lets the uiautomator2 driver fetch the
26
- # Chromedriver matching the Cordova WebView's Chrome version on demand. Without it,
27
- # switching to the WEBVIEW context fails ("No Chromedriver found that can automate
28
- # Chrome 'X'"), which breaks every bridge test in its `before all` hook.
29
- # Detach Appium's stdout/stderr (it logs to --log anyway): if it keeps the script's
30
- # output pipe open, the android-emulator-runner action hangs after the tests finish
31
- # instead of returning.
32
- npx appium --allow-insecure=uiautomator2:chromedriver_autodownload \
33
- --log "$LOGDIR/appium-android.log" --log-level info >/dev/null 2>&1 &
34
- APPIUM_PID=$!
35
- trap 'kill $APPIUM_PID 2>/dev/null || true' EXIT
36
- # Wait for Appium to accept connections.
37
- for i in $(seq 1 30); do
38
- curl -sf http://127.0.0.1:4723/status >/dev/null 2>&1 && break
39
- sleep 1
40
- done
41
-
42
- run_suite() { # $1 = spec glob, $2 = hard|soft
43
- local spec="$1" gate="$2" tries=3 n=1
44
- while [ $n -le $tries ]; do
45
- echo "== [$gate] $spec (attempt $n/$tries) =="
46
- if PURCHASELY_E2E_SPEC="$spec" npx wdio run ./wdio.android.conf.js --spec "$spec" 2>&1 | tee "$LOGDIR/wdio-$(basename "$spec").log"; then
47
- return 0
48
- fi
49
- n=$((n+1))
50
- done
51
- if [ "$gate" = "hard" ]; then
52
- echo "::error::E2E suite failed (hard gate): $spec"
53
- return 1
54
- fi
55
- echo "::warning::E2E suite failed (best-effort): $spec"
56
- return 0
57
- }
58
-
59
- cd "$HERE"
60
- rc=0
61
- run_suite "./specs/bridge.e2e.js" hard || rc=1
62
- run_suite "./specs/dismiss.e2e.js" soft || true
63
- exit $rc
@@ -1,56 +0,0 @@
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
- # Appium 2 loads drivers from APPIUM_HOME (~/.appium), not node_modules, so make sure the
17
- # xcuitest driver is registered (idempotent; a no-op where it is already installed).
18
- echo "== Ensuring xcuitest driver is installed =="
19
- npx appium driver install xcuitest 2>/dev/null || true
20
-
21
- echo "== Starting Appium =="
22
- # Detach Appium's stdout/stderr (it logs to --log anyway) so it can't hold the runner's
23
- # output pipe open after the tests finish.
24
- npx appium --log "$LOGDIR/appium-ios.log" --log-level info >/dev/null 2>&1 &
25
- APPIUM_PID=$!
26
- trap 'kill $APPIUM_PID 2>/dev/null || true' EXIT
27
- for i in $(seq 1 30); do
28
- curl -sf http://127.0.0.1:4723/status >/dev/null 2>&1 && break
29
- sleep 1
30
- done
31
-
32
- run_suite() { # $1 = spec, $2 = hard|soft
33
- # More retries than Android: WebDriverAgent's cold first build (~10-13 min) can outlast a
34
- # few session-creation attempts, and the hard-gate bridge spec runs first — extra tries let
35
- # WDA finish building (it is then cached via derivedDataPath, so later specs are instant).
36
- local spec="$1" gate="$2" tries="${E2E_TRIES:-6}" n=1
37
- while [ $n -le $tries ]; do
38
- echo "== [$gate] $spec (attempt $n/$tries) =="
39
- if npx wdio run ./wdio.ios.conf.js --spec "$spec" 2>&1 | tee "$LOGDIR/wdio-$(basename "$spec").log"; then
40
- return 0
41
- fi
42
- n=$((n+1))
43
- done
44
- if [ "$gate" = "hard" ]; then
45
- echo "::error::E2E suite failed (hard gate): $spec"
46
- return 1
47
- fi
48
- echo "::warning::E2E suite failed (best-effort): $spec"
49
- return 0
50
- }
51
-
52
- cd "$HERE"
53
- rc=0
54
- run_suite "./specs/bridge.e2e.js" hard || rc=1
55
- run_suite "./specs/dismiss.e2e.js" soft || true
56
- exit $rc