react-native-revnix 1.4.2 → 1.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.
- 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 +12 -0
- package/dist/client.js +68 -0
- package/dist/esm/client.js +68 -0
- 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 +35 -8
- 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 +13 -3
- package/dist/ui/RevnixPaywall.js +35 -8
- 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 +87 -0
- package/src/index.ts +5 -0
- package/src/store/index.ts +44 -0
- package/src/store.ts +41 -0
- package/src/ui/RevnixPaywall.tsx +54 -17
- package/src/version.ts +1 -1
- package/store.d.ts +1 -0
- package/store.js +1 -0
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, 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;
|
package/dist/ui/RevnixPaywall.js
CHANGED
|
@@ -54,6 +54,7 @@ exports.RevnixPaywall = RevnixPaywall;
|
|
|
54
54
|
const React = __importStar(require("react"));
|
|
55
55
|
const react_native_1 = require("react-native");
|
|
56
56
|
const implicit_js_1 = require("../implicit.js");
|
|
57
|
+
const store_js_1 = require("../store.js");
|
|
57
58
|
const color_js_1 = require("../blocks/color.js");
|
|
58
59
|
const RevnixBlockPaywall_js_1 = require("./RevnixBlockPaywall.js");
|
|
59
60
|
/** `config.blocks` is opaque on the core config (the non-UI entrypoint must
|
|
@@ -178,7 +179,7 @@ function RevnixPaywall(props) {
|
|
|
178
179
|
});
|
|
179
180
|
onSelectPackage?.(packageId);
|
|
180
181
|
}, [onSelectPackage, report, productOf]);
|
|
181
|
-
const { onPurchase } = props;
|
|
182
|
+
const { onPurchase, onPurchaseCompleted } = props;
|
|
182
183
|
// Rises per attempt so a retry after a failure is its own occurrence rather
|
|
183
184
|
// than being absorbed as a duplicate of the first try.
|
|
184
185
|
const attemptRef = React.useRef(0);
|
|
@@ -205,9 +206,26 @@ function RevnixPaywall(props) {
|
|
|
205
206
|
eventId: occurrence,
|
|
206
207
|
});
|
|
207
208
|
};
|
|
209
|
+
const purchase = onPurchase ??
|
|
210
|
+
(async () => {
|
|
211
|
+
if (!productId) {
|
|
212
|
+
return { status: "failed", message: "package has no productId" };
|
|
213
|
+
}
|
|
214
|
+
if (!client?.purchase) {
|
|
215
|
+
return { status: "failed", message: "pass client or onPurchase" };
|
|
216
|
+
}
|
|
217
|
+
const result = await client.purchase(productId, {
|
|
218
|
+
viewId: viewIdRef.current,
|
|
219
|
+
placementKey,
|
|
220
|
+
paywallId,
|
|
221
|
+
});
|
|
222
|
+
if (result)
|
|
223
|
+
onPurchaseCompleted?.(result);
|
|
224
|
+
return { status: result ? "completed" : "cancelled" };
|
|
225
|
+
});
|
|
208
226
|
let returned;
|
|
209
227
|
try {
|
|
210
|
-
returned =
|
|
228
|
+
returned = purchase(packageId);
|
|
211
229
|
}
|
|
212
230
|
catch (err) {
|
|
213
231
|
// A host that throws synchronously has failed the purchase as surely
|
|
@@ -220,14 +238,23 @@ function RevnixPaywall(props) {
|
|
|
220
238
|
return;
|
|
221
239
|
}
|
|
222
240
|
settle(returned);
|
|
223
|
-
}, [onPurchase, report, productOf, placementKey]);
|
|
241
|
+
}, [onPurchase, onPurchaseCompleted, report, productOf, placementKey, paywallId, client]);
|
|
224
242
|
const { onRestore } = props;
|
|
225
|
-
const handleRestore = React.useMemo(() =>
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
243
|
+
const handleRestore = React.useMemo(() => {
|
|
244
|
+
if (onRestore) {
|
|
245
|
+
return () => {
|
|
246
|
+
report("restore");
|
|
247
|
+
onRestore();
|
|
248
|
+
};
|
|
229
249
|
}
|
|
230
|
-
|
|
250
|
+
const restore = client?.restore?.bind(client);
|
|
251
|
+
if (!restore || !(0, store_js_1.getStoreAdapter)())
|
|
252
|
+
return undefined;
|
|
253
|
+
return () => {
|
|
254
|
+
report("restore");
|
|
255
|
+
void restore().catch((err) => client?.reportRenderDiagnostic?.(String(err?.message ?? err)));
|
|
256
|
+
};
|
|
257
|
+
}, [onRestore, report, client]);
|
|
231
258
|
// The host's dismissal runs whatever happens to the beacon — an analytics
|
|
232
259
|
// failure must never trap the customer on the screen (REV-252).
|
|
233
260
|
const { onClose } = props;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.
|
|
1
|
+
export declare const SDK_VERSION = "1.5.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.5.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.5.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
|
package/src/client.ts
CHANGED
|
@@ -20,6 +20,11 @@ import {
|
|
|
20
20
|
encodeDeviceHeader,
|
|
21
21
|
} from "./device.js";
|
|
22
22
|
import { SDK_VERSION } from "./version.js";
|
|
23
|
+
import {
|
|
24
|
+
getStoreAdapter,
|
|
25
|
+
type StoreAdapter,
|
|
26
|
+
type StoreTransaction,
|
|
27
|
+
} from "./store.js";
|
|
23
28
|
import {
|
|
24
29
|
browserLifecycle,
|
|
25
30
|
DEFAULT_SESSION_TIMEOUT_MS,
|
|
@@ -185,6 +190,7 @@ export class RevnixClient {
|
|
|
185
190
|
private implicitConfigPromise: Promise<ReadonlySet<string>> | null = null;
|
|
186
191
|
private implicitConfigRetryAt = 0;
|
|
187
192
|
private lifecycleUnsubscribe: (() => void) | null = null;
|
|
193
|
+
private stopStoreObserver: (() => void) | null = null;
|
|
188
194
|
/** The cold-start batch while it runs, resolving to whether it presented a
|
|
189
195
|
* paywall. A deep link delivered on the first frame — SwiftUI's onOpenURL,
|
|
190
196
|
* RN's getInitialURL — must wait for it, or the customer gets the launch
|
|
@@ -253,6 +259,14 @@ export class RevnixClient {
|
|
|
253
259
|
void this.retryPendingPurchases().catch((err) =>
|
|
254
260
|
this.diag("retryPendingPurchases", err)
|
|
255
261
|
);
|
|
262
|
+
const store = getStoreAdapter();
|
|
263
|
+
if (store) {
|
|
264
|
+
this.stopStoreObserver = store.observe((tx) =>
|
|
265
|
+
void this.registerStoreTransaction(store, tx).catch((err) =>
|
|
266
|
+
this.diag("storeObserver", err)
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
}
|
|
256
270
|
// REV-157: warm the placement cache at launch, so the first paywall of the
|
|
257
271
|
// session can render offline. Fire-and-forget; a failed key just stays on
|
|
258
272
|
// whatever an earlier launch cached.
|
|
@@ -425,6 +439,77 @@ export class RevnixClient {
|
|
|
425
439
|
}
|
|
426
440
|
}
|
|
427
441
|
|
|
442
|
+
/** Buys `productId` through the native store (StoreKit 2 / Play Billing),
|
|
443
|
+
* registers it with Revnix, then finishes the store transaction. Resolves
|
|
444
|
+
* null when the customer cancelled or the purchase is pending approval.
|
|
445
|
+
* Needs `import "react-native-revnix/store"` before configure(). A refused
|
|
446
|
+
* claim (PurchaseBlockedError) is never finished, so Google refunds it. */
|
|
447
|
+
async purchase(
|
|
448
|
+
productId: string,
|
|
449
|
+
opts?: Pick<RegisterPurchaseInput, "viewId" | "paywallId" | "placementKey">
|
|
450
|
+
): Promise<RegisterPurchaseResult | null> {
|
|
451
|
+
const store = this.requireStore();
|
|
452
|
+
const customerId = await this.customerIdPromise;
|
|
453
|
+
const tx = await store.purchase(productId, { customerId });
|
|
454
|
+
if (!tx) return null;
|
|
455
|
+
return this.registerStoreTransaction(store, tx, opts);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Re-registers every purchase the store still holds for this device and
|
|
459
|
+
* resolves how many registered. Per-purchase failures go to onDiagnostic. */
|
|
460
|
+
async restore(): Promise<number> {
|
|
461
|
+
const store = this.requireStore();
|
|
462
|
+
let restored = 0;
|
|
463
|
+
for (const tx of await store.restore()) {
|
|
464
|
+
try {
|
|
465
|
+
await this.registerStoreTransaction(store, tx, undefined, tx.store === "google");
|
|
466
|
+
restored += 1;
|
|
467
|
+
} catch (err) {
|
|
468
|
+
this.diag("storeRestore", err);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return restored;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private requireStore(): StoreAdapter {
|
|
475
|
+
const store = getStoreAdapter();
|
|
476
|
+
if (!store) {
|
|
477
|
+
throw new Error(
|
|
478
|
+
'Revnix: native purchases need `import "react-native-revnix/store"` before Revnix.configure(...) and a rebuilt app (pod install / Expo development build)'
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
return store;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private async registerStoreTransaction(
|
|
485
|
+
store: StoreAdapter,
|
|
486
|
+
tx: StoreTransaction,
|
|
487
|
+
opts?: Pick<RegisterPurchaseInput, "viewId" | "paywallId" | "placementKey">,
|
|
488
|
+
finish = true
|
|
489
|
+
): Promise<RegisterPurchaseResult> {
|
|
490
|
+
const finishTx = () =>
|
|
491
|
+
finish
|
|
492
|
+
? store.finish(tx.transactionId).catch((err) => this.diag("storeFinish", err))
|
|
493
|
+
: undefined;
|
|
494
|
+
try {
|
|
495
|
+
const result = await this.registerPurchase({
|
|
496
|
+
source: tx.store,
|
|
497
|
+
token: tx.token,
|
|
498
|
+
productId: tx.productId,
|
|
499
|
+
transactionId: tx.transactionId,
|
|
500
|
+
occurredAt: tx.occurredAt,
|
|
501
|
+
expiresAt: tx.expiresAt,
|
|
502
|
+
signedTransactionInfo: tx.signedTransactionInfo,
|
|
503
|
+
...opts,
|
|
504
|
+
});
|
|
505
|
+
await finishTx();
|
|
506
|
+
return result;
|
|
507
|
+
} catch (err) {
|
|
508
|
+
if (!(err instanceof PurchaseBlockedError)) await finishTx();
|
|
509
|
+
throw err;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
428
513
|
private postPurchase(
|
|
429
514
|
customerId: string,
|
|
430
515
|
input: RegisterPurchaseInput
|
|
@@ -1532,6 +1617,8 @@ export class RevnixClient {
|
|
|
1532
1617
|
// every return alongside its successor.
|
|
1533
1618
|
this.lifecycleUnsubscribe?.();
|
|
1534
1619
|
this.lifecycleUnsubscribe = null;
|
|
1620
|
+
this.stopStoreObserver?.();
|
|
1621
|
+
this.stopStoreObserver = null;
|
|
1535
1622
|
}
|
|
1536
1623
|
|
|
1537
1624
|
/** A block paywall reporting a paint string it could not read. Routed to the
|
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
|
+
}
|