react-native-revnix 1.4.2 → 1.6.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.
- package/README.md +138 -713
- package/android/build.gradle +34 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/kotlin/com/revnix/reactnative/RevnixStoreModule.kt +247 -0
- package/android/src/main/kotlin/com/revnix/reactnative/RevnixStorePackage.kt +14 -0
- package/dist/client.d.ts +58 -11
- package/dist/client.js +403 -114
- package/dist/esm/client.js +402 -114
- package/dist/esm/index.js +1 -0
- package/dist/esm/store/index.js +22 -0
- package/dist/esm/store.js +9 -0
- package/dist/esm/ui/RevnixPaywall.js +42 -8
- package/dist/esm/ui/index.js +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.js +25 -0
- package/dist/store.d.ts +30 -0
- package/dist/store.js +13 -0
- package/dist/ui/RevnixPaywall.d.ts +17 -3
- package/dist/ui/RevnixPaywall.js +43 -8
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/index.js +2 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/RevnixStore.m +18 -0
- package/ios/RevnixStore.swift +106 -0
- package/package.json +20 -2
- package/react-native-revnix.podspec +22 -0
- package/src/client.ts +475 -146
- package/src/index.ts +5 -0
- package/src/store/index.ts +44 -0
- package/src/store.ts +41 -0
- package/src/ui/RevnixPaywall.tsx +68 -17
- package/src/ui/index.ts +1 -0
- package/src/version.ts +1 -1
- package/store.d.ts +1 -0
- package/store.js +1 -0
package/src/index.ts
CHANGED
|
@@ -12,6 +12,11 @@ export {
|
|
|
12
12
|
RevnixTimeoutError,
|
|
13
13
|
} from "./errors.js";
|
|
14
14
|
export { purchaseInputFromIap, type IapPurchase } from "./iap.js";
|
|
15
|
+
export {
|
|
16
|
+
setStoreAdapter,
|
|
17
|
+
type StoreAdapter,
|
|
18
|
+
type StoreTransaction,
|
|
19
|
+
} from "./store.js";
|
|
15
20
|
export { memoryStorage, generateAnonymousId } from "./storage.js";
|
|
16
21
|
export { SDK_VERSION } from "./version.js";
|
|
17
22
|
export { DEVICE_HEADER, encodeDeviceHeader } from "./device.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NativeEventEmitter, NativeModules } from "react-native";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
setStoreAdapter,
|
|
5
|
+
type StoreAdapter,
|
|
6
|
+
type StoreTransaction,
|
|
7
|
+
} from "../store.js";
|
|
8
|
+
|
|
9
|
+
interface NativeRevnixStore {
|
|
10
|
+
purchase(productId: string, customerId: string): Promise<StoreTransaction | null>;
|
|
11
|
+
restore(): Promise<StoreTransaction[]>;
|
|
12
|
+
finish(transactionId: string): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const native: NativeRevnixStore | undefined = NativeModules.RevnixStore;
|
|
16
|
+
|
|
17
|
+
const missing = (): Promise<never> =>
|
|
18
|
+
Promise.reject(
|
|
19
|
+
new Error(
|
|
20
|
+
"Revnix: RevnixStore native module not found. Run pod install / rebuild the app (Expo: use a development build)."
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export const RevnixStore: StoreAdapter = native
|
|
25
|
+
? {
|
|
26
|
+
purchase: (productId, { customerId }) => native.purchase(productId, customerId),
|
|
27
|
+
restore: () => native.restore(),
|
|
28
|
+
finish: (transactionId) => native.finish(transactionId),
|
|
29
|
+
observe(onTransaction) {
|
|
30
|
+
const sub = new NativeEventEmitter(NativeModules.RevnixStore).addListener(
|
|
31
|
+
"revnixStoreTransaction",
|
|
32
|
+
(tx) => onTransaction(tx as StoreTransaction)
|
|
33
|
+
);
|
|
34
|
+
return () => sub.remove();
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
: {
|
|
38
|
+
purchase: missing,
|
|
39
|
+
restore: missing,
|
|
40
|
+
finish: missing,
|
|
41
|
+
observe: () => () => {},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (native) setStoreAdapter(RevnixStore);
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Store } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export interface StoreTransaction {
|
|
4
|
+
store: Store;
|
|
5
|
+
productId: string;
|
|
6
|
+
/** Apple: transaction id · Google: purchaseToken. */
|
|
7
|
+
transactionId: string;
|
|
8
|
+
/** Apple: originalTransactionId · Google: purchaseToken. */
|
|
9
|
+
token: string;
|
|
10
|
+
/** Unix ms. */
|
|
11
|
+
occurredAt?: number;
|
|
12
|
+
/** Unix ms. */
|
|
13
|
+
expiresAt?: number;
|
|
14
|
+
/** Apple only: the StoreKit 2 JWS. */
|
|
15
|
+
signedTransactionInfo?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface StoreAdapter {
|
|
19
|
+
/** Resolves null when the customer cancelled or the purchase is pending. */
|
|
20
|
+
purchase(
|
|
21
|
+
productId: string,
|
|
22
|
+
opts: { customerId: string }
|
|
23
|
+
): Promise<StoreTransaction | null>;
|
|
24
|
+
restore(): Promise<StoreTransaction[]>;
|
|
25
|
+
finish(transactionId: string): Promise<void>;
|
|
26
|
+
/** Transactions arriving outside purchase(): renewals, other devices, Ask
|
|
27
|
+
* to Buy, refunds. Returns an unsubscribe. */
|
|
28
|
+
observe(onTransaction: (tx: StoreTransaction) => void): () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let adapter: StoreAdapter | null = null;
|
|
32
|
+
|
|
33
|
+
/** Installs the store that `RevnixClient.purchase()` / `restore()` drive.
|
|
34
|
+
* `import "react-native-revnix/store"` calls this with the native bridge. */
|
|
35
|
+
export function setStoreAdapter(next: StoreAdapter | null): void {
|
|
36
|
+
adapter = next;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getStoreAdapter(): StoreAdapter | null {
|
|
40
|
+
return adapter;
|
|
41
|
+
}
|
package/src/ui/RevnixPaywall.tsx
CHANGED
|
@@ -34,8 +34,12 @@ import type {
|
|
|
34
34
|
PaywallConfig,
|
|
35
35
|
PaywallInteraction,
|
|
36
36
|
PaywallPurchaseOutcome,
|
|
37
|
+
PlacementResolution,
|
|
38
|
+
RegisterPurchaseResult,
|
|
37
39
|
} from "../types.js";
|
|
38
40
|
import { PREVIEW_PLACEMENT_KEY } from "../implicit.js";
|
|
41
|
+
import { getStoreAdapter } from "../store.js";
|
|
42
|
+
import { paywallImageUrls, type RevnixClient } from "../client.js";
|
|
39
43
|
import type { PaywallBlockDoc } from "../blocks/types.js";
|
|
40
44
|
import type { FontFamilyMap } from "../blocks/style.js";
|
|
41
45
|
import { withAlpha } from "../blocks/color.js";
|
|
@@ -142,8 +146,11 @@ export interface RevnixPaywallProps {
|
|
|
142
146
|
* `paywall.purchase_failed`, and a promise that REJECTS is reported as a
|
|
143
147
|
* failure carrying the error's message. Returning nothing keeps today's
|
|
144
148
|
* behavior exactly — the start is reported, the outcome is not, and you
|
|
145
|
-
* can still report it yourself with `client.logPaywallEvent`.
|
|
146
|
-
|
|
149
|
+
* can still report it yourself with `client.logPaywallEvent`.
|
|
150
|
+
*
|
|
151
|
+
* Omit it to let the paywall buy through `client.purchase()`, which needs
|
|
152
|
+
* `import "react-native-revnix/store"`. */
|
|
153
|
+
onPurchase?: (
|
|
147
154
|
packageId: string
|
|
148
155
|
) => void | PaywallPurchaseOutcome | Promise<PaywallPurchaseOutcome | void>;
|
|
149
156
|
/** Controlled selection; omit to let the paywall manage it (initial
|
|
@@ -152,7 +159,10 @@ export interface RevnixPaywallProps {
|
|
|
152
159
|
onSelectPackage?: (packageId: string) => void;
|
|
153
160
|
/** Renders a spinner in the CTA and disables purchasing. */
|
|
154
161
|
loading?: boolean;
|
|
162
|
+
/** Omit it to restore through `client.restore()`. */
|
|
155
163
|
onRestore?: () => void;
|
|
164
|
+
/** Fires when the paywall's own purchase (no `onPurchase`) succeeds. */
|
|
165
|
+
onPurchaseCompleted?: (result: RegisterPurchaseResult) => void;
|
|
156
166
|
onTerms?: () => void;
|
|
157
167
|
onPrivacy?: () => void;
|
|
158
168
|
/** Called when the customer dismisses the paywall (REV-252) — either by
|
|
@@ -201,6 +211,9 @@ export interface RevnixPaywallProps {
|
|
|
201
211
|
eventId?: string;
|
|
202
212
|
}
|
|
203
213
|
) => void;
|
|
214
|
+
/** Used when `onPurchase` / `onRestore` are omitted. */
|
|
215
|
+
purchase?: RevnixClient["purchase"];
|
|
216
|
+
restore?: RevnixClient["restore"];
|
|
204
217
|
};
|
|
205
218
|
/** Placement/paywall attribution attached to the view report. */
|
|
206
219
|
placementKey?: string;
|
|
@@ -243,6 +256,19 @@ export interface RevnixPaywallProps {
|
|
|
243
256
|
locale?: string;
|
|
244
257
|
}
|
|
245
258
|
|
|
259
|
+
/** Warms React Native's image cache with every image `resolution`'s paywall
|
|
260
|
+
* loads, so it paints without waiting on the network. Pair it with
|
|
261
|
+
* `client.preload` / `resolvePlacement`. Never throws. */
|
|
262
|
+
export async function prefetchPaywallImages(
|
|
263
|
+
resolution: PlacementResolution | null
|
|
264
|
+
): Promise<void> {
|
|
265
|
+
await Promise.all(
|
|
266
|
+
paywallImageUrls(resolution?.paywall).map((url) =>
|
|
267
|
+
Image.prefetch(url).catch(() => false)
|
|
268
|
+
)
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
246
272
|
export function RevnixPaywall(props: RevnixPaywallProps) {
|
|
247
273
|
// One view per mount: remounting (a re-shown paywall) is a genuine new
|
|
248
274
|
// display; re-renders are not.
|
|
@@ -338,7 +364,7 @@ export function RevnixPaywall(props: RevnixPaywallProps) {
|
|
|
338
364
|
[onSelectPackage, report, productOf]
|
|
339
365
|
);
|
|
340
366
|
|
|
341
|
-
const { onPurchase } = props;
|
|
367
|
+
const { onPurchase, onPurchaseCompleted } = props;
|
|
342
368
|
// Rises per attempt so a retry after a failure is its own occurrence rather
|
|
343
369
|
// than being absorbed as a duplicate of the first try.
|
|
344
370
|
const attemptRef = React.useRef(0);
|
|
@@ -368,9 +394,26 @@ export function RevnixPaywall(props: RevnixPaywallProps) {
|
|
|
368
394
|
}
|
|
369
395
|
);
|
|
370
396
|
};
|
|
371
|
-
|
|
397
|
+
const purchase =
|
|
398
|
+
onPurchase ??
|
|
399
|
+
(async (): Promise<PaywallPurchaseOutcome> => {
|
|
400
|
+
if (!productId) {
|
|
401
|
+
return { status: "failed", message: "package has no productId" };
|
|
402
|
+
}
|
|
403
|
+
if (!client?.purchase) {
|
|
404
|
+
return { status: "failed", message: "pass client or onPurchase" };
|
|
405
|
+
}
|
|
406
|
+
const result = await client.purchase(productId, {
|
|
407
|
+
viewId: viewIdRef.current,
|
|
408
|
+
placementKey,
|
|
409
|
+
paywallId,
|
|
410
|
+
});
|
|
411
|
+
if (result) onPurchaseCompleted?.(result);
|
|
412
|
+
return { status: result ? "completed" : "cancelled" };
|
|
413
|
+
});
|
|
414
|
+
let returned: ReturnType<typeof purchase>;
|
|
372
415
|
try {
|
|
373
|
-
returned =
|
|
416
|
+
returned = purchase(packageId);
|
|
374
417
|
} catch (err) {
|
|
375
418
|
// A host that throws synchronously has failed the purchase as surely
|
|
376
419
|
// as one whose promise rejects.
|
|
@@ -386,20 +429,26 @@ export function RevnixPaywall(props: RevnixPaywallProps) {
|
|
|
386
429
|
}
|
|
387
430
|
settle(returned as PaywallPurchaseOutcome | void);
|
|
388
431
|
},
|
|
389
|
-
[onPurchase, report, productOf, placementKey]
|
|
432
|
+
[onPurchase, onPurchaseCompleted, report, productOf, placementKey, paywallId, client]
|
|
390
433
|
);
|
|
391
434
|
|
|
392
435
|
const { onRestore } = props;
|
|
393
|
-
const handleRestore = React.useMemo(
|
|
394
|
-
()
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
436
|
+
const handleRestore = React.useMemo(() => {
|
|
437
|
+
if (onRestore) {
|
|
438
|
+
return () => {
|
|
439
|
+
report("restore");
|
|
440
|
+
onRestore();
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const restore = client?.restore?.bind(client);
|
|
444
|
+
if (!restore || !getStoreAdapter()) return undefined;
|
|
445
|
+
return () => {
|
|
446
|
+
report("restore");
|
|
447
|
+
void restore().catch((err) =>
|
|
448
|
+
client?.reportRenderDiagnostic?.(String(err?.message ?? err))
|
|
449
|
+
);
|
|
450
|
+
};
|
|
451
|
+
}, [onRestore, report, client]);
|
|
403
452
|
|
|
404
453
|
// The host's dismissal runs whatever happens to the beacon — an analytics
|
|
405
454
|
// failure must never trap the customer on the screen (REV-252).
|
|
@@ -532,7 +581,9 @@ function ClassicClose({
|
|
|
532
581
|
|
|
533
582
|
/** The nine `template` layouts — every paywall published before the block
|
|
534
583
|
* builder, and the fallback for any config that carries no block tree. */
|
|
535
|
-
function ClassicPaywall(
|
|
584
|
+
function ClassicPaywall(
|
|
585
|
+
props: RevnixPaywallProps & { onPurchase: (packageId: string) => void }
|
|
586
|
+
) {
|
|
536
587
|
const { config, packages, loading } = props;
|
|
537
588
|
// Base scheme comes from the dashboard config (mode: dark|light, absent =
|
|
538
589
|
// dark for legacy configs); the host app's explicit theme prop wins on top.
|
package/src/ui/index.ts
CHANGED
package/src/version.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// L3: the single source of the SDK version inside src/. Must match
|
|
2
2
|
// package.json "version" — version.test.ts fails the build when they drift
|
|
3
3
|
// (they had: client.ts said 0.7.0 while the package shipped 0.9.0).
|
|
4
|
-
export const SDK_VERSION = "1.
|
|
4
|
+
export const SDK_VERSION = "1.6.0";
|
package/store.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./dist/store/index";
|
package/store.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require("./dist/store/index.js");
|