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/dist/esm/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { setLocale } from "./blocks/locale.js";
|
|
|
3
3
|
export { RevnixClient } from "./client.js";
|
|
4
4
|
export { PurchaseBlockedError, RevnixAuthError, RevnixError, RevnixNetworkError, RevnixRateLimitError, RevnixTimeoutError, } from "./errors.js";
|
|
5
5
|
export { purchaseInputFromIap } from "./iap.js";
|
|
6
|
+
export { setStoreAdapter, } from "./store.js";
|
|
6
7
|
export { memoryStorage, generateAnonymousId } from "./storage.js";
|
|
7
8
|
export { SDK_VERSION } from "./version.js";
|
|
8
9
|
export { DEVICE_HEADER, encodeDeviceHeader } from "./device.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NativeEventEmitter, NativeModules } from "react-native";
|
|
2
|
+
import { setStoreAdapter, } from "../store.js";
|
|
3
|
+
const native = NativeModules.RevnixStore;
|
|
4
|
+
const missing = () => Promise.reject(new Error("Revnix: RevnixStore native module not found. Run pod install / rebuild the app (Expo: use a development build)."));
|
|
5
|
+
export const RevnixStore = native
|
|
6
|
+
? {
|
|
7
|
+
purchase: (productId, { customerId }) => native.purchase(productId, customerId),
|
|
8
|
+
restore: () => native.restore(),
|
|
9
|
+
finish: (transactionId) => native.finish(transactionId),
|
|
10
|
+
observe(onTransaction) {
|
|
11
|
+
const sub = new NativeEventEmitter(NativeModules.RevnixStore).addListener("revnixStoreTransaction", (tx) => onTransaction(tx));
|
|
12
|
+
return () => sub.remove();
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
: {
|
|
16
|
+
purchase: missing,
|
|
17
|
+
restore: missing,
|
|
18
|
+
finish: missing,
|
|
19
|
+
observe: () => () => { },
|
|
20
|
+
};
|
|
21
|
+
if (native)
|
|
22
|
+
setStoreAdapter(RevnixStore);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
let adapter = null;
|
|
2
|
+
/** Installs the store that `RevnixClient.purchase()` / `restore()` drive.
|
|
3
|
+
* `import "react-native-revnix/store"` calls this with the native bridge. */
|
|
4
|
+
export function setStoreAdapter(next) {
|
|
5
|
+
adapter = next;
|
|
6
|
+
}
|
|
7
|
+
export function getStoreAdapter() {
|
|
8
|
+
return adapter;
|
|
9
|
+
}
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
import * as React from "react";
|
|
18
18
|
import { ActivityIndicator, Alert, Image, Linking, Pressable, ScrollView, StyleSheet, Text, View, } from "react-native";
|
|
19
19
|
import { PREVIEW_PLACEMENT_KEY } from "../implicit.js";
|
|
20
|
+
import { getStoreAdapter } from "../store.js";
|
|
21
|
+
import { paywallImageUrls } from "../client.js";
|
|
20
22
|
import { withAlpha } from "../blocks/color.js";
|
|
21
23
|
import { RevnixBlockPaywall, } from "./RevnixBlockPaywall.js";
|
|
22
24
|
/** `config.blocks` is opaque on the core config (the non-UI entrypoint must
|
|
@@ -65,6 +67,12 @@ const CURRENCY_SYMBOL = /[$€£¥₹₩₽₺₫₪฿₴₦₱]/;
|
|
|
65
67
|
// part of the public theme — derived from the config's mode, in lockstep
|
|
66
68
|
// with the dashboard preview's SCREEN_PALETTES.card.
|
|
67
69
|
const CARD_BG = { dark: "#181b22", light: "#f4f5f7" };
|
|
70
|
+
/** Warms React Native's image cache with every image `resolution`'s paywall
|
|
71
|
+
* loads, so it paints without waiting on the network. Pair it with
|
|
72
|
+
* `client.preload` / `resolvePlacement`. Never throws. */
|
|
73
|
+
export async function prefetchPaywallImages(resolution) {
|
|
74
|
+
await Promise.all(paywallImageUrls(resolution?.paywall).map((url) => Image.prefetch(url).catch(() => false)));
|
|
75
|
+
}
|
|
68
76
|
export function RevnixPaywall(props) {
|
|
69
77
|
// One view per mount: remounting (a re-shown paywall) is a genuine new
|
|
70
78
|
// display; re-renders are not.
|
|
@@ -141,7 +149,7 @@ export function RevnixPaywall(props) {
|
|
|
141
149
|
});
|
|
142
150
|
onSelectPackage?.(packageId);
|
|
143
151
|
}, [onSelectPackage, report, productOf]);
|
|
144
|
-
const { onPurchase } = props;
|
|
152
|
+
const { onPurchase, onPurchaseCompleted } = props;
|
|
145
153
|
// Rises per attempt so a retry after a failure is its own occurrence rather
|
|
146
154
|
// than being absorbed as a duplicate of the first try.
|
|
147
155
|
const attemptRef = React.useRef(0);
|
|
@@ -168,9 +176,26 @@ export function RevnixPaywall(props) {
|
|
|
168
176
|
eventId: occurrence,
|
|
169
177
|
});
|
|
170
178
|
};
|
|
179
|
+
const purchase = onPurchase ??
|
|
180
|
+
(async () => {
|
|
181
|
+
if (!productId) {
|
|
182
|
+
return { status: "failed", message: "package has no productId" };
|
|
183
|
+
}
|
|
184
|
+
if (!client?.purchase) {
|
|
185
|
+
return { status: "failed", message: "pass client or onPurchase" };
|
|
186
|
+
}
|
|
187
|
+
const result = await client.purchase(productId, {
|
|
188
|
+
viewId: viewIdRef.current,
|
|
189
|
+
placementKey,
|
|
190
|
+
paywallId,
|
|
191
|
+
});
|
|
192
|
+
if (result)
|
|
193
|
+
onPurchaseCompleted?.(result);
|
|
194
|
+
return { status: result ? "completed" : "cancelled" };
|
|
195
|
+
});
|
|
171
196
|
let returned;
|
|
172
197
|
try {
|
|
173
|
-
returned =
|
|
198
|
+
returned = purchase(packageId);
|
|
174
199
|
}
|
|
175
200
|
catch (err) {
|
|
176
201
|
// A host that throws synchronously has failed the purchase as surely
|
|
@@ -183,14 +208,23 @@ export function RevnixPaywall(props) {
|
|
|
183
208
|
return;
|
|
184
209
|
}
|
|
185
210
|
settle(returned);
|
|
186
|
-
}, [onPurchase, report, productOf, placementKey]);
|
|
211
|
+
}, [onPurchase, onPurchaseCompleted, report, productOf, placementKey, paywallId, client]);
|
|
187
212
|
const { onRestore } = props;
|
|
188
|
-
const handleRestore = React.useMemo(() =>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
213
|
+
const handleRestore = React.useMemo(() => {
|
|
214
|
+
if (onRestore) {
|
|
215
|
+
return () => {
|
|
216
|
+
report("restore");
|
|
217
|
+
onRestore();
|
|
218
|
+
};
|
|
192
219
|
}
|
|
193
|
-
|
|
220
|
+
const restore = client?.restore?.bind(client);
|
|
221
|
+
if (!restore || !getStoreAdapter())
|
|
222
|
+
return undefined;
|
|
223
|
+
return () => {
|
|
224
|
+
report("restore");
|
|
225
|
+
void restore().catch((err) => client?.reportRenderDiagnostic?.(String(err?.message ?? err)));
|
|
226
|
+
};
|
|
227
|
+
}, [onRestore, report, client]);
|
|
194
228
|
// The host's dismissal runs whatever happens to the beacon — an analytics
|
|
195
229
|
// failure must never trap the customer on the screen (REV-252).
|
|
196
230
|
const { onClose } = props;
|
package/dist/esm/ui/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { RevnixPaywall, DEFAULT_PAYWALL_THEME, LIGHT_PAYWALL_THEME, } from "./RevnixPaywall.js";
|
|
1
|
+
export { RevnixPaywall, DEFAULT_PAYWALL_THEME, LIGHT_PAYWALL_THEME, prefetchPaywallImages, } from "./RevnixPaywall.js";
|
|
2
2
|
export { RevnixBlockPaywall, } from "./RevnixBlockPaywall.js";
|
|
3
3
|
export { DESIGN_CANVAS, } from "../blocks/types.js";
|
|
4
4
|
export { backgroundBaseColor, backgroundLayers, coverPlacement, } from "../blocks/background.js";
|
package/dist/esm/version.js
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/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { RevnixConfig } from "./types.js";
|
|
|
4
4
|
export { RevnixClient } from "./client.js";
|
|
5
5
|
export { PurchaseBlockedError, RevnixAuthError, RevnixError, RevnixNetworkError, RevnixRateLimitError, RevnixTimeoutError, } from "./errors.js";
|
|
6
6
|
export { purchaseInputFromIap, type IapPurchase } from "./iap.js";
|
|
7
|
+
export { setStoreAdapter, type StoreAdapter, type StoreTransaction, } from "./store.js";
|
|
7
8
|
export { memoryStorage, generateAnonymousId } from "./storage.js";
|
|
8
9
|
export { SDK_VERSION } from "./version.js";
|
|
9
10
|
export { DEVICE_HEADER, encodeDeviceHeader } from "./device.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Revnix = exports.PREVIEW_PLACEMENT_KEY = exports.IMPLICIT_PLACEMENTS = exports.HOST_CALL_PLACEMENT = exports.DEFAULT_SESSION_TIMEOUT_MS = exports.browserLifecycle = exports.encodeDeviceHeader = exports.DEVICE_HEADER = exports.SDK_VERSION = exports.generateAnonymousId = exports.memoryStorage = exports.purchaseInputFromIap = exports.RevnixTimeoutError = exports.RevnixRateLimitError = exports.RevnixNetworkError = exports.RevnixError = exports.RevnixAuthError = exports.PurchaseBlockedError = exports.RevnixClient = void 0;
|
|
3
|
+
exports.Revnix = exports.PREVIEW_PLACEMENT_KEY = exports.IMPLICIT_PLACEMENTS = exports.HOST_CALL_PLACEMENT = exports.DEFAULT_SESSION_TIMEOUT_MS = exports.browserLifecycle = exports.encodeDeviceHeader = exports.DEVICE_HEADER = exports.SDK_VERSION = exports.generateAnonymousId = exports.memoryStorage = exports.setStoreAdapter = exports.purchaseInputFromIap = exports.RevnixTimeoutError = exports.RevnixRateLimitError = exports.RevnixNetworkError = exports.RevnixError = exports.RevnixAuthError = exports.PurchaseBlockedError = exports.RevnixClient = void 0;
|
|
4
4
|
const client_js_1 = require("./client.js");
|
|
5
5
|
const locale_js_1 = require("./blocks/locale.js");
|
|
6
6
|
var client_js_2 = require("./client.js");
|
|
@@ -14,6 +14,8 @@ Object.defineProperty(exports, "RevnixRateLimitError", { enumerable: true, get:
|
|
|
14
14
|
Object.defineProperty(exports, "RevnixTimeoutError", { enumerable: true, get: function () { return errors_js_1.RevnixTimeoutError; } });
|
|
15
15
|
var iap_js_1 = require("./iap.js");
|
|
16
16
|
Object.defineProperty(exports, "purchaseInputFromIap", { enumerable: true, get: function () { return iap_js_1.purchaseInputFromIap; } });
|
|
17
|
+
var store_js_1 = require("./store.js");
|
|
18
|
+
Object.defineProperty(exports, "setStoreAdapter", { enumerable: true, get: function () { return store_js_1.setStoreAdapter; } });
|
|
17
19
|
var storage_js_1 = require("./storage.js");
|
|
18
20
|
Object.defineProperty(exports, "memoryStorage", { enumerable: true, get: function () { return storage_js_1.memoryStorage; } });
|
|
19
21
|
Object.defineProperty(exports, "generateAnonymousId", { enumerable: true, get: function () { return storage_js_1.generateAnonymousId; } });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RevnixStore = void 0;
|
|
4
|
+
const react_native_1 = require("react-native");
|
|
5
|
+
const store_js_1 = require("../store.js");
|
|
6
|
+
const native = react_native_1.NativeModules.RevnixStore;
|
|
7
|
+
const missing = () => Promise.reject(new Error("Revnix: RevnixStore native module not found. Run pod install / rebuild the app (Expo: use a development build)."));
|
|
8
|
+
exports.RevnixStore = native
|
|
9
|
+
? {
|
|
10
|
+
purchase: (productId, { customerId }) => native.purchase(productId, customerId),
|
|
11
|
+
restore: () => native.restore(),
|
|
12
|
+
finish: (transactionId) => native.finish(transactionId),
|
|
13
|
+
observe(onTransaction) {
|
|
14
|
+
const sub = new react_native_1.NativeEventEmitter(react_native_1.NativeModules.RevnixStore).addListener("revnixStoreTransaction", (tx) => onTransaction(tx));
|
|
15
|
+
return () => sub.remove();
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
: {
|
|
19
|
+
purchase: missing,
|
|
20
|
+
restore: missing,
|
|
21
|
+
finish: missing,
|
|
22
|
+
observe: () => () => { },
|
|
23
|
+
};
|
|
24
|
+
if (native)
|
|
25
|
+
(0, store_js_1.setStoreAdapter)(exports.RevnixStore);
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Store } from "./types.js";
|
|
2
|
+
export interface StoreTransaction {
|
|
3
|
+
store: Store;
|
|
4
|
+
productId: string;
|
|
5
|
+
/** Apple: transaction id · Google: purchaseToken. */
|
|
6
|
+
transactionId: string;
|
|
7
|
+
/** Apple: originalTransactionId · Google: purchaseToken. */
|
|
8
|
+
token: string;
|
|
9
|
+
/** Unix ms. */
|
|
10
|
+
occurredAt?: number;
|
|
11
|
+
/** Unix ms. */
|
|
12
|
+
expiresAt?: number;
|
|
13
|
+
/** Apple only: the StoreKit 2 JWS. */
|
|
14
|
+
signedTransactionInfo?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface StoreAdapter {
|
|
17
|
+
/** Resolves null when the customer cancelled or the purchase is pending. */
|
|
18
|
+
purchase(productId: string, opts: {
|
|
19
|
+
customerId: string;
|
|
20
|
+
}): Promise<StoreTransaction | null>;
|
|
21
|
+
restore(): Promise<StoreTransaction[]>;
|
|
22
|
+
finish(transactionId: string): Promise<void>;
|
|
23
|
+
/** Transactions arriving outside purchase(): renewals, other devices, Ask
|
|
24
|
+
* to Buy, refunds. Returns an unsubscribe. */
|
|
25
|
+
observe(onTransaction: (tx: StoreTransaction) => void): () => void;
|
|
26
|
+
}
|
|
27
|
+
/** Installs the store that `RevnixClient.purchase()` / `restore()` drive.
|
|
28
|
+
* `import "react-native-revnix/store"` calls this with the native bridge. */
|
|
29
|
+
export declare function setStoreAdapter(next: StoreAdapter | null): void;
|
|
30
|
+
export declare function getStoreAdapter(): StoreAdapter | null;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setStoreAdapter = setStoreAdapter;
|
|
4
|
+
exports.getStoreAdapter = getStoreAdapter;
|
|
5
|
+
let adapter = null;
|
|
6
|
+
/** Installs the store that `RevnixClient.purchase()` / `restore()` drive.
|
|
7
|
+
* `import "react-native-revnix/store"` calls this with the native bridge. */
|
|
8
|
+
function setStoreAdapter(next) {
|
|
9
|
+
adapter = next;
|
|
10
|
+
}
|
|
11
|
+
function getStoreAdapter() {
|
|
12
|
+
return adapter;
|
|
13
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
-
import type { PaywallConfig, PaywallInteraction, PaywallPurchaseOutcome } from "../types.js";
|
|
3
|
+
import type { PaywallConfig, PaywallInteraction, PaywallPurchaseOutcome, PlacementResolution, RegisterPurchaseResult } from "../types.js";
|
|
4
|
+
import { type RevnixClient } from "../client.js";
|
|
4
5
|
import type { FontFamilyMap } from "../blocks/style.js";
|
|
5
6
|
import { type RevnixBlurComponent, type RevnixGradientComponent } from "./RevnixBlockPaywall.js";
|
|
6
7
|
/** One purchasable row. `priceLabel` must come from the store (localized). */
|
|
@@ -51,15 +52,21 @@ export interface RevnixPaywallProps {
|
|
|
51
52
|
* `paywall.purchase_failed`, and a promise that REJECTS is reported as a
|
|
52
53
|
* failure carrying the error's message. Returning nothing keeps today's
|
|
53
54
|
* behavior exactly — the start is reported, the outcome is not, and you
|
|
54
|
-
* can still report it yourself with `client.logPaywallEvent`.
|
|
55
|
-
|
|
55
|
+
* can still report it yourself with `client.logPaywallEvent`.
|
|
56
|
+
*
|
|
57
|
+
* Omit it to let the paywall buy through `client.purchase()`, which needs
|
|
58
|
+
* `import "react-native-revnix/store"`. */
|
|
59
|
+
onPurchase?: (packageId: string) => void | PaywallPurchaseOutcome | Promise<PaywallPurchaseOutcome | void>;
|
|
56
60
|
/** Controlled selection; omit to let the paywall manage it (initial
|
|
57
61
|
* selection is the config's highlight package, else the first package). */
|
|
58
62
|
selectedPackageId?: string;
|
|
59
63
|
onSelectPackage?: (packageId: string) => void;
|
|
60
64
|
/** Renders a spinner in the CTA and disables purchasing. */
|
|
61
65
|
loading?: boolean;
|
|
66
|
+
/** Omit it to restore through `client.restore()`. */
|
|
62
67
|
onRestore?: () => void;
|
|
68
|
+
/** Fires when the paywall's own purchase (no `onPurchase`) succeeds. */
|
|
69
|
+
onPurchaseCompleted?: (result: RegisterPurchaseResult) => void;
|
|
63
70
|
onTerms?: () => void;
|
|
64
71
|
onPrivacy?: () => void;
|
|
65
72
|
/** Called when the customer dismisses the paywall (REV-252) — either by
|
|
@@ -104,6 +111,9 @@ export interface RevnixPaywallProps {
|
|
|
104
111
|
message?: string;
|
|
105
112
|
eventId?: string;
|
|
106
113
|
}) => void;
|
|
114
|
+
/** Used when `onPurchase` / `onRestore` are omitted. */
|
|
115
|
+
purchase?: RevnixClient["purchase"];
|
|
116
|
+
restore?: RevnixClient["restore"];
|
|
107
117
|
};
|
|
108
118
|
/** Placement/paywall attribution attached to the view report. */
|
|
109
119
|
placementKey?: string;
|
|
@@ -145,4 +155,8 @@ export interface RevnixPaywallProps {
|
|
|
145
155
|
* layouts below render `config`'s own copy whatever this says. */
|
|
146
156
|
locale?: string;
|
|
147
157
|
}
|
|
158
|
+
/** Warms React Native's image cache with every image `resolution`'s paywall
|
|
159
|
+
* loads, so it paints without waiting on the network. Pair it with
|
|
160
|
+
* `client.preload` / `resolvePlacement`. Never throws. */
|
|
161
|
+
export declare function prefetchPaywallImages(resolution: PlacementResolution | null): Promise<void>;
|
|
148
162
|
export declare function RevnixPaywall(props: RevnixPaywallProps): React.JSX.Element;
|
package/dist/ui/RevnixPaywall.js
CHANGED
|
@@ -50,10 +50,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
50
50
|
})();
|
|
51
51
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
52
|
exports.LIGHT_PAYWALL_THEME = exports.DEFAULT_PAYWALL_THEME = void 0;
|
|
53
|
+
exports.prefetchPaywallImages = prefetchPaywallImages;
|
|
53
54
|
exports.RevnixPaywall = RevnixPaywall;
|
|
54
55
|
const React = __importStar(require("react"));
|
|
55
56
|
const react_native_1 = require("react-native");
|
|
56
57
|
const implicit_js_1 = require("../implicit.js");
|
|
58
|
+
const store_js_1 = require("../store.js");
|
|
59
|
+
const client_js_1 = require("../client.js");
|
|
57
60
|
const color_js_1 = require("../blocks/color.js");
|
|
58
61
|
const RevnixBlockPaywall_js_1 = require("./RevnixBlockPaywall.js");
|
|
59
62
|
/** `config.blocks` is opaque on the core config (the non-UI entrypoint must
|
|
@@ -102,6 +105,12 @@ const CURRENCY_SYMBOL = /[$€£¥₹₩₽₺₫₪฿₴₦₱]/;
|
|
|
102
105
|
// part of the public theme — derived from the config's mode, in lockstep
|
|
103
106
|
// with the dashboard preview's SCREEN_PALETTES.card.
|
|
104
107
|
const CARD_BG = { dark: "#181b22", light: "#f4f5f7" };
|
|
108
|
+
/** Warms React Native's image cache with every image `resolution`'s paywall
|
|
109
|
+
* loads, so it paints without waiting on the network. Pair it with
|
|
110
|
+
* `client.preload` / `resolvePlacement`. Never throws. */
|
|
111
|
+
async function prefetchPaywallImages(resolution) {
|
|
112
|
+
await Promise.all((0, client_js_1.paywallImageUrls)(resolution?.paywall).map((url) => react_native_1.Image.prefetch(url).catch(() => false)));
|
|
113
|
+
}
|
|
105
114
|
function RevnixPaywall(props) {
|
|
106
115
|
// One view per mount: remounting (a re-shown paywall) is a genuine new
|
|
107
116
|
// display; re-renders are not.
|
|
@@ -178,7 +187,7 @@ function RevnixPaywall(props) {
|
|
|
178
187
|
});
|
|
179
188
|
onSelectPackage?.(packageId);
|
|
180
189
|
}, [onSelectPackage, report, productOf]);
|
|
181
|
-
const { onPurchase } = props;
|
|
190
|
+
const { onPurchase, onPurchaseCompleted } = props;
|
|
182
191
|
// Rises per attempt so a retry after a failure is its own occurrence rather
|
|
183
192
|
// than being absorbed as a duplicate of the first try.
|
|
184
193
|
const attemptRef = React.useRef(0);
|
|
@@ -205,9 +214,26 @@ function RevnixPaywall(props) {
|
|
|
205
214
|
eventId: occurrence,
|
|
206
215
|
});
|
|
207
216
|
};
|
|
217
|
+
const purchase = onPurchase ??
|
|
218
|
+
(async () => {
|
|
219
|
+
if (!productId) {
|
|
220
|
+
return { status: "failed", message: "package has no productId" };
|
|
221
|
+
}
|
|
222
|
+
if (!client?.purchase) {
|
|
223
|
+
return { status: "failed", message: "pass client or onPurchase" };
|
|
224
|
+
}
|
|
225
|
+
const result = await client.purchase(productId, {
|
|
226
|
+
viewId: viewIdRef.current,
|
|
227
|
+
placementKey,
|
|
228
|
+
paywallId,
|
|
229
|
+
});
|
|
230
|
+
if (result)
|
|
231
|
+
onPurchaseCompleted?.(result);
|
|
232
|
+
return { status: result ? "completed" : "cancelled" };
|
|
233
|
+
});
|
|
208
234
|
let returned;
|
|
209
235
|
try {
|
|
210
|
-
returned =
|
|
236
|
+
returned = purchase(packageId);
|
|
211
237
|
}
|
|
212
238
|
catch (err) {
|
|
213
239
|
// A host that throws synchronously has failed the purchase as surely
|
|
@@ -220,14 +246,23 @@ function RevnixPaywall(props) {
|
|
|
220
246
|
return;
|
|
221
247
|
}
|
|
222
248
|
settle(returned);
|
|
223
|
-
}, [onPurchase, report, productOf, placementKey]);
|
|
249
|
+
}, [onPurchase, onPurchaseCompleted, report, productOf, placementKey, paywallId, client]);
|
|
224
250
|
const { onRestore } = props;
|
|
225
|
-
const handleRestore = React.useMemo(() =>
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
251
|
+
const handleRestore = React.useMemo(() => {
|
|
252
|
+
if (onRestore) {
|
|
253
|
+
return () => {
|
|
254
|
+
report("restore");
|
|
255
|
+
onRestore();
|
|
256
|
+
};
|
|
229
257
|
}
|
|
230
|
-
|
|
258
|
+
const restore = client?.restore?.bind(client);
|
|
259
|
+
if (!restore || !(0, store_js_1.getStoreAdapter)())
|
|
260
|
+
return undefined;
|
|
261
|
+
return () => {
|
|
262
|
+
report("restore");
|
|
263
|
+
void restore().catch((err) => client?.reportRenderDiagnostic?.(String(err?.message ?? err)));
|
|
264
|
+
};
|
|
265
|
+
}, [onRestore, report, client]);
|
|
231
266
|
// The host's dismissal runs whatever happens to the beacon — an analytics
|
|
232
267
|
// failure must never trap the customer on the screen (REV-252).
|
|
233
268
|
const { onClose } = props;
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { RevnixPaywall, DEFAULT_PAYWALL_THEME, LIGHT_PAYWALL_THEME, type RevnixPaywallPackage, type RevnixPaywallProps, type RevnixPaywallTheme, } from "./RevnixPaywall.js";
|
|
1
|
+
export { RevnixPaywall, DEFAULT_PAYWALL_THEME, LIGHT_PAYWALL_THEME, prefetchPaywallImages, type RevnixPaywallPackage, type RevnixPaywallProps, type RevnixPaywallTheme, } from "./RevnixPaywall.js";
|
|
2
2
|
export { RevnixBlockPaywall, type RevnixBlockPaywallProps, type RevnixBlurComponent, type RevnixBlurProps, type RevnixGradientComponent, type RevnixGradientProps, } from "./RevnixBlockPaywall.js";
|
|
3
3
|
export { DESIGN_CANVAS, type BlockStyle, type PaywallBlock, type PaywallBlockDoc, } from "../blocks/types.js";
|
|
4
4
|
export { backgroundBaseColor, backgroundLayers, coverPlacement, type BackgroundFit, type BackgroundImage, type BackgroundLayers, type BackgroundOverlay, type PaywallBackground, type PaywallBackgroundSpec, } from "../blocks/background.js";
|
package/dist/ui/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveTags = exports.minorUnitsForCurrency = exports.setLocale = exports.normalizeLocale = exports.localizeBlockDoc = exports.localeChain = exports.linkLabels = exports.detectLocale = exports.coverPlacement = exports.backgroundLayers = exports.backgroundBaseColor = exports.DESIGN_CANVAS = exports.RevnixBlockPaywall = exports.LIGHT_PAYWALL_THEME = exports.DEFAULT_PAYWALL_THEME = exports.RevnixPaywall = void 0;
|
|
3
|
+
exports.resolveTags = exports.minorUnitsForCurrency = exports.setLocale = exports.normalizeLocale = exports.localizeBlockDoc = exports.localeChain = exports.linkLabels = exports.detectLocale = exports.coverPlacement = exports.backgroundLayers = exports.backgroundBaseColor = exports.DESIGN_CANVAS = exports.RevnixBlockPaywall = exports.prefetchPaywallImages = exports.LIGHT_PAYWALL_THEME = exports.DEFAULT_PAYWALL_THEME = exports.RevnixPaywall = void 0;
|
|
4
4
|
var RevnixPaywall_js_1 = require("./RevnixPaywall.js");
|
|
5
5
|
Object.defineProperty(exports, "RevnixPaywall", { enumerable: true, get: function () { return RevnixPaywall_js_1.RevnixPaywall; } });
|
|
6
6
|
Object.defineProperty(exports, "DEFAULT_PAYWALL_THEME", { enumerable: true, get: function () { return RevnixPaywall_js_1.DEFAULT_PAYWALL_THEME; } });
|
|
7
7
|
Object.defineProperty(exports, "LIGHT_PAYWALL_THEME", { enumerable: true, get: function () { return RevnixPaywall_js_1.LIGHT_PAYWALL_THEME; } });
|
|
8
|
+
Object.defineProperty(exports, "prefetchPaywallImages", { enumerable: true, get: function () { return RevnixPaywall_js_1.prefetchPaywallImages; } });
|
|
8
9
|
var RevnixBlockPaywall_js_1 = require("./RevnixBlockPaywall.js");
|
|
9
10
|
Object.defineProperty(exports, "RevnixBlockPaywall", { enumerable: true, get: function () { return RevnixBlockPaywall_js_1.RevnixBlockPaywall; } });
|
|
10
11
|
var types_js_1 = require("../blocks/types.js");
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.
|
|
1
|
+
export declare const SDK_VERSION = "1.6.0";
|
package/dist/version.js
CHANGED
|
@@ -4,4 +4,4 @@ exports.SDK_VERSION = void 0;
|
|
|
4
4
|
// L3: the single source of the SDK version inside src/. Must match
|
|
5
5
|
// package.json "version" — version.test.ts fails the build when they drift
|
|
6
6
|
// (they had: client.ts said 0.7.0 while the package shipped 0.9.0).
|
|
7
|
-
exports.SDK_VERSION = "1.
|
|
7
|
+
exports.SDK_VERSION = "1.6.0";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
|
|
4
|
+
@interface RCT_EXTERN_MODULE(RevnixStore, RCTEventEmitter)
|
|
5
|
+
|
|
6
|
+
RCT_EXTERN_METHOD(purchase:(NSString *)productId
|
|
7
|
+
customerId:(NSString *)customerId
|
|
8
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
9
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
10
|
+
|
|
11
|
+
RCT_EXTERN_METHOD(restore:(RCTPromiseResolveBlock)resolve
|
|
12
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
13
|
+
|
|
14
|
+
RCT_EXTERN_METHOD(finish:(NSString *)transactionId
|
|
15
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
16
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
17
|
+
|
|
18
|
+
@end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import React
|
|
3
|
+
import StoreKit
|
|
4
|
+
|
|
5
|
+
@available(iOS 15.0, *)
|
|
6
|
+
@objc(RevnixStore)
|
|
7
|
+
class RevnixStore: RCTEventEmitter {
|
|
8
|
+
private var updates: Task<Void, Never>?
|
|
9
|
+
|
|
10
|
+
@objc override static func requiresMainQueueSetup() -> Bool {
|
|
11
|
+
false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override func supportedEvents() -> [String]! {
|
|
15
|
+
["revnixStoreTransaction"]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override func startObserving() {
|
|
19
|
+
updates?.cancel()
|
|
20
|
+
updates = Task { [weak self] in
|
|
21
|
+
for await update in Transaction.updates where update.unsafePayloadValue.revocationDate == nil {
|
|
22
|
+
self?.sendEvent(withName: "revnixStoreTransaction", body: RevnixStore.payload(update))
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override func stopObserving() {
|
|
28
|
+
updates?.cancel()
|
|
29
|
+
updates = nil
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@objc(purchase:customerId:resolver:rejecter:)
|
|
33
|
+
func purchase(
|
|
34
|
+
_ productId: String,
|
|
35
|
+
customerId: String,
|
|
36
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
37
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
38
|
+
) {
|
|
39
|
+
Task {
|
|
40
|
+
do {
|
|
41
|
+
guard let product = try await Product.products(for: [productId]).first else {
|
|
42
|
+
reject("product_not_found", "No App Store product with id \(productId)", nil)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
switch try await product.purchase() {
|
|
46
|
+
case .success(let verification):
|
|
47
|
+
resolve(RevnixStore.payload(verification))
|
|
48
|
+
default:
|
|
49
|
+
resolve(nil)
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
reject("store_error", error.localizedDescription, error)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@objc(restore:rejecter:)
|
|
58
|
+
func restore(
|
|
59
|
+
_ resolve: @escaping RCTPromiseResolveBlock,
|
|
60
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
61
|
+
) {
|
|
62
|
+
Task {
|
|
63
|
+
try? await AppStore.sync()
|
|
64
|
+
var transactions: [[String: Any]] = []
|
|
65
|
+
for await entitlement in Transaction.currentEntitlements
|
|
66
|
+
where entitlement.unsafePayloadValue.revocationDate == nil {
|
|
67
|
+
transactions.append(RevnixStore.payload(entitlement))
|
|
68
|
+
}
|
|
69
|
+
resolve(transactions)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@objc(finish:resolver:rejecter:)
|
|
74
|
+
func finish(
|
|
75
|
+
_ transactionId: String,
|
|
76
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
77
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
78
|
+
) {
|
|
79
|
+
Task {
|
|
80
|
+
for await unfinished in Transaction.unfinished {
|
|
81
|
+
let transaction = unfinished.unsafePayloadValue
|
|
82
|
+
if String(transaction.id) == transactionId {
|
|
83
|
+
await transaction.finish()
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
resolve(nil)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private static func payload(_ verification: VerificationResult<Transaction>) -> [String: Any] {
|
|
92
|
+
let transaction = verification.unsafePayloadValue
|
|
93
|
+
var payload: [String: Any] = [
|
|
94
|
+
"store": "apple",
|
|
95
|
+
"productId": transaction.productID,
|
|
96
|
+
"transactionId": String(transaction.id),
|
|
97
|
+
"token": String(transaction.originalID),
|
|
98
|
+
"occurredAt": Int(transaction.purchaseDate.timeIntervalSince1970 * 1000),
|
|
99
|
+
"signedTransactionInfo": verification.jwsRepresentation,
|
|
100
|
+
]
|
|
101
|
+
if let expiresAt = transaction.expirationDate {
|
|
102
|
+
payload["expiresAt"] = Int(expiresAt.timeIntervalSince1970 * 1000)
|
|
103
|
+
}
|
|
104
|
+
return payload
|
|
105
|
+
}
|
|
106
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-revnix",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Revnix SDK for React, React Native & Next.js: purchase registration, entitlements, identity, and placements over the Revnix /v1 API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/Oth-tech/revnix-sdk.git"
|
|
9
9
|
},
|
|
10
|
+
"homepage": "https://www.revnix.io/docs/react-native",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"email": "support@revnix.io"
|
|
13
|
+
},
|
|
10
14
|
"keywords": [
|
|
11
15
|
"revnix",
|
|
12
16
|
"react-native",
|
|
@@ -24,7 +28,11 @@
|
|
|
24
28
|
"main": "dist/index.js",
|
|
25
29
|
"module": "dist/esm/index.js",
|
|
26
30
|
"types": "dist/index.d.ts",
|
|
27
|
-
"sideEffects":
|
|
31
|
+
"sideEffects": [
|
|
32
|
+
"./store.js",
|
|
33
|
+
"./dist/store/index.js",
|
|
34
|
+
"./dist/esm/store/index.js"
|
|
35
|
+
],
|
|
28
36
|
"exports": {
|
|
29
37
|
".": {
|
|
30
38
|
"types": "./dist/index.d.ts",
|
|
@@ -36,6 +44,11 @@
|
|
|
36
44
|
"import": "./dist/esm/ui/index.js",
|
|
37
45
|
"require": "./ui.js"
|
|
38
46
|
},
|
|
47
|
+
"./store": {
|
|
48
|
+
"types": "./store.d.ts",
|
|
49
|
+
"import": "./dist/esm/store/index.js",
|
|
50
|
+
"require": "./store.js"
|
|
51
|
+
},
|
|
39
52
|
"./package.json": "./package.json"
|
|
40
53
|
},
|
|
41
54
|
"files": [
|
|
@@ -43,6 +56,11 @@
|
|
|
43
56
|
"src",
|
|
44
57
|
"ui.js",
|
|
45
58
|
"ui.d.ts",
|
|
59
|
+
"store.js",
|
|
60
|
+
"store.d.ts",
|
|
61
|
+
"ios",
|
|
62
|
+
"android",
|
|
63
|
+
"react-native-revnix.podspec",
|
|
46
64
|
"!src/**/*.test.ts"
|
|
47
65
|
],
|
|
48
66
|
"scripts": {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "react-native-revnix"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.author = "Revnix"
|
|
12
|
+
s.platforms = { :ios => "15.1" }
|
|
13
|
+
s.source = { :git => "https://github.com/Oth-tech/revnix-sdk.git", :tag => "v#{s.version}" }
|
|
14
|
+
s.source_files = "ios/**/*.{h,m,swift}"
|
|
15
|
+
s.swift_version = "5.0"
|
|
16
|
+
|
|
17
|
+
if respond_to?(:install_modules_dependencies, true)
|
|
18
|
+
install_modules_dependencies(s)
|
|
19
|
+
else
|
|
20
|
+
s.dependency "React-Core"
|
|
21
|
+
end
|
|
22
|
+
end
|