@chipmobilesdk/rn-payment 0.4.0 → 0.5.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.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * What any `ProviderPort` must do (IOS-12, PAY-13).
3
+ *
4
+ * The same checks run against the fake store and against the Play adapter over a
5
+ * simulated binding (or the real one, from a demo screen on a device), so a test
6
+ * that drives the fake proves something about the device path. Each check builds
7
+ * its own port over the same small store, through a harness.
8
+ */
9
+ import {
10
+ ensure,
11
+ runChecks,
12
+ type Check,
13
+ type ConformanceReport,
14
+ } from '@chipmobilesdk/rn-core/testing';
15
+
16
+ import type { ProviderPort } from '../adapter/port';
17
+ import type { ProductType } from '../config/types';
18
+
19
+ export interface StoreProductSpec {
20
+ readonly storeProductId: string;
21
+ readonly productType: ProductType;
22
+ /** As the store formats it; the port must pass it through untouched. */
23
+ readonly formattedPrice: string;
24
+ }
25
+
26
+ export interface OwnedPurchaseSpec {
27
+ readonly storeProductId: string;
28
+ readonly token: string;
29
+ readonly state: 'purchased' | 'pending';
30
+ }
31
+
32
+ export interface StoreScenario {
33
+ readonly products: readonly StoreProductSpec[];
34
+ readonly owned: readonly OwnedPurchaseSpec[];
35
+ }
36
+
37
+ export interface ProviderSubject {
38
+ readonly port: ProviderPort;
39
+ /** How the next purchase flow the user starts ends. */
40
+ nextPurchaseEnds(outcome: 'bought' | 'cancelled'): void;
41
+ /** The billing service goes away underneath the app. */
42
+ dropService(): void;
43
+ }
44
+
45
+ export interface ProviderHarness {
46
+ create(scenario: StoreScenario): ProviderSubject;
47
+ }
48
+
49
+ /** The store every check runs against. */
50
+ export const CONFORMANCE_STORE: StoreScenario = {
51
+ products: [
52
+ {
53
+ storeProductId: 'conformance.remove_ads',
54
+ productType: 'one_time_permanent',
55
+ formattedPrice: '€1.99',
56
+ },
57
+ {
58
+ storeProductId: 'conformance.coins',
59
+ productType: 'one_time_consumable',
60
+ formattedPrice: '€0.99',
61
+ },
62
+ ],
63
+ owned: [
64
+ { storeProductId: 'conformance.remove_ads', token: 'token-owned', state: 'purchased' },
65
+ { storeProductId: 'conformance.coins', token: 'token-pending', state: 'pending' },
66
+ ],
67
+ };
68
+
69
+ export const providerPortChecks: readonly Check<ProviderHarness>[] = [
70
+ {
71
+ name: 'connects, and disconnects safely before a connect and twice',
72
+ async run(harness) {
73
+ const { port } = harness.create(CONFORMANCE_STORE);
74
+ await port.disconnect();
75
+ const outcome = await port.connect();
76
+ ensure(outcome.ok, `connect failed: ${JSON.stringify(outcome)}`);
77
+ await port.disconnect();
78
+ await port.disconnect();
79
+ },
80
+ },
81
+ {
82
+ name: 'reports a dropped service to observers, and nothing after unsubscribing',
83
+ async run(harness) {
84
+ const subject = harness.create(CONFORMANCE_STORE);
85
+ const seen: boolean[] = [];
86
+ const stop = subject.port.observeConnection(connected => seen.push(connected));
87
+ await subject.port.connect();
88
+ ensure(seen.at(-1) === true, `after connect observers saw ${JSON.stringify(seen)}`);
89
+ subject.dropService();
90
+ ensure(seen.at(-1) === false, `after the drop observers saw ${JSON.stringify(seen)}`);
91
+ stop();
92
+ const before = seen.length;
93
+ await subject.port.connect();
94
+ ensure(seen.length === before, 'notified after unsubscribing');
95
+ },
96
+ },
97
+ {
98
+ name: 'returns the requested products with the store-formatted price',
99
+ async run(harness) {
100
+ const { port } = harness.create(CONFORMANCE_STORE);
101
+ await port.connect();
102
+ const products = await port.queryProducts(['conformance.remove_ads'], 'one_time_permanent');
103
+ ensure(products.length === 1, `expected one product, got ${products.length}`);
104
+ const [product] = products;
105
+ ensure(product?.storeProductId === 'conformance.remove_ads', 'wrong product returned');
106
+ ensure(product.productType === 'one_time_permanent', `product type ${product.productType}`);
107
+ ensure(product.formattedPrice === '€1.99', `price ${product.formattedPrice}`);
108
+ },
109
+ },
110
+ {
111
+ name: 'flags a pending purchase rather than filtering it out',
112
+ async run(harness) {
113
+ const { port } = harness.create(CONFORMANCE_STORE);
114
+ await port.connect();
115
+ const owned = await port.queryOwned();
116
+ const pending = owned.find(purchase => purchase.token === 'token-pending');
117
+ const bought = owned.find(purchase => purchase.token === 'token-owned');
118
+ ensure(pending?.state === 'pending', `pending purchase reported as ${pending?.state}`);
119
+ ensure(bought?.state === 'purchased', `bought purchase reported as ${bought?.state}`);
120
+ },
121
+ },
122
+ {
123
+ name: 'reports a bought purchase for the product that was requested',
124
+ async run(harness) {
125
+ const subject = harness.create(CONFORMANCE_STORE);
126
+ await subject.port.connect();
127
+ subject.nextPurchaseEnds('bought');
128
+ const outcome = await subject.port.launchPurchase({
129
+ storeProductId: 'conformance.coins',
130
+ productType: 'one_time_consumable',
131
+ });
132
+ ensure(outcome.ok, `purchase failed: ${JSON.stringify(outcome)}`);
133
+ ensure(
134
+ outcome.purchase.storeProductId === 'conformance.coins',
135
+ 'purchase for another product',
136
+ );
137
+ ensure(outcome.purchase.token !== '', 'purchase has no token to finalize');
138
+ },
139
+ },
140
+ {
141
+ name: 'reports a cancelled flow as user_cancelled',
142
+ async run(harness) {
143
+ const subject = harness.create(CONFORMANCE_STORE);
144
+ await subject.port.connect();
145
+ subject.nextPurchaseEnds('cancelled');
146
+ const outcome = await subject.port.launchPurchase({
147
+ storeProductId: 'conformance.remove_ads',
148
+ productType: 'one_time_permanent',
149
+ });
150
+ ensure(!outcome.ok, 'a cancelled flow reported a purchase');
151
+ ensure(outcome.reason === 'user_cancelled', `reason ${outcome.reason}`);
152
+ },
153
+ },
154
+ {
155
+ name: 'acknowledges and consumes safely twice',
156
+ async run(harness) {
157
+ const { port } = harness.create(CONFORMANCE_STORE);
158
+ await port.connect();
159
+ await port.acknowledge('token-owned');
160
+ await port.acknowledge('token-owned');
161
+ await port.consume('token-pending');
162
+ await port.consume('token-pending');
163
+ },
164
+ },
165
+ ];
166
+
167
+ export function runProviderPortConformance(harness: ProviderHarness): Promise<ConformanceReport> {
168
+ return runChecks(() => harness, providerPortChecks);
169
+ }