react-native-revnix 1.8.0 → 1.9.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 +1 -0
- package/android/src/main/kotlin/com/revnix/reactnative/RevnixStoreModule.kt +29 -9
- package/dist/client.d.ts +2 -0
- package/dist/client.js +43 -5
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +11 -1
- package/dist/esm/client.js +44 -6
- package/dist/esm/errors.js +9 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/store/index.js +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/store/index.js +1 -1
- package/dist/store.d.ts +7 -1
- package/dist/types.d.ts +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/RevnixStore.m +1 -0
- package/ios/RevnixStore.swift +3 -2
- package/package.json +1 -1
- package/src/client.ts +48 -7
- package/src/errors.ts +10 -0
- package/src/index.ts +1 -0
- package/src/store/index.ts +2 -2
- package/src/store.ts +7 -1
- package/src/types.ts +3 -0
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -104,6 +104,7 @@ Your API key and deployment URL are in the dashboard under Settings. See [API ke
|
|
|
104
104
|
**Revnix handles the hard parts of subscriptions in a small, developer-friendly SDK.**
|
|
105
105
|
|
|
106
106
|
- `purchase(productId)` runs the whole flow: store sheet, server-side receipt validation, entitlement, transaction finish. `restore()` brings purchases back on a new device.
|
|
107
|
+
- Consumables on Google Play: list their ids in `consumableProductIds` so they are consumed after Revnix records the purchase and can be bought again.
|
|
107
108
|
- Renewals, purchases on other devices and Ask to Buy approvals arrive on their own and are registered automatically.
|
|
108
109
|
- Entitlements and placements are cached on device, so paid users stay unlocked offline.
|
|
109
110
|
- Failed registrations are retried automatically when the app comes back online.
|
|
@@ -9,6 +9,7 @@ import com.android.billingclient.api.BillingClient.ProductType
|
|
|
9
9
|
import com.android.billingclient.api.BillingClientStateListener
|
|
10
10
|
import com.android.billingclient.api.BillingFlowParams
|
|
11
11
|
import com.android.billingclient.api.BillingResult
|
|
12
|
+
import com.android.billingclient.api.ConsumeParams
|
|
12
13
|
import com.android.billingclient.api.PendingPurchasesParams
|
|
13
14
|
import com.android.billingclient.api.ProductDetails
|
|
14
15
|
import com.android.billingclient.api.Purchase
|
|
@@ -96,10 +97,9 @@ class RevnixStoreModule(context: ReactApplicationContext) :
|
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
// ponytail: acknowledges every purchase; consumable INAPP products need consumeAsync to be bought again
|
|
100
100
|
@ReactMethod
|
|
101
|
-
fun finish(purchaseToken: String, promise: Promise) {
|
|
102
|
-
if (purchaseToken in acknowledged) {
|
|
101
|
+
fun finish(purchaseToken: String, consume: Boolean, promise: Promise) {
|
|
102
|
+
if (!consume && purchaseToken in acknowledged) {
|
|
103
103
|
promise.resolve(null)
|
|
104
104
|
return
|
|
105
105
|
}
|
|
@@ -108,13 +108,32 @@ class RevnixStoreModule(context: ReactApplicationContext) :
|
|
|
108
108
|
promise.reject("store_error", "${setup.responseCode} ${setup.debugMessage}")
|
|
109
109
|
return@withBilling
|
|
110
110
|
}
|
|
111
|
-
val
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
val acknowledge = { onResult: (BillingResult) -> Unit ->
|
|
112
|
+
val params = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchaseToken).build()
|
|
113
|
+
billing.acknowledgePurchase(params) { result ->
|
|
114
|
+
if (result.responseCode == BillingResponseCode.OK) acknowledged.add(purchaseToken)
|
|
115
|
+
onResult(result)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!consume) {
|
|
119
|
+
acknowledge { result ->
|
|
120
|
+
if (result.responseCode == BillingResponseCode.OK) promise.resolve(null)
|
|
121
|
+
else promise.reject("store_error", "acknowledgePurchase failed: ${result.responseCode} ${result.debugMessage}")
|
|
122
|
+
}
|
|
123
|
+
return@withBilling
|
|
124
|
+
}
|
|
125
|
+
val params = ConsumeParams.newBuilder().setPurchaseToken(purchaseToken).build()
|
|
126
|
+
billing.consumeAsync(params) { consumed, _ ->
|
|
127
|
+
if (consumed.responseCode == BillingResponseCode.OK) {
|
|
114
128
|
acknowledged.add(purchaseToken)
|
|
115
129
|
promise.resolve(null)
|
|
116
130
|
} else {
|
|
117
|
-
|
|
131
|
+
acknowledge { acked ->
|
|
132
|
+
promise.reject(
|
|
133
|
+
"store_error",
|
|
134
|
+
"consumeAsync failed: ${consumed.responseCode} ${consumed.debugMessage}; acknowledgePurchase fallback: ${acked.responseCode} ${acked.debugMessage}",
|
|
135
|
+
)
|
|
136
|
+
}
|
|
118
137
|
}
|
|
119
138
|
}
|
|
120
139
|
}
|
|
@@ -193,7 +212,7 @@ class RevnixStoreModule(context: ReactApplicationContext) :
|
|
|
193
212
|
if (result.responseCode == BillingResponseCode.OK) {
|
|
194
213
|
queryPurchases(ProductType.SUBS) { subs ->
|
|
195
214
|
queryPurchases(ProductType.INAPP) { inapp ->
|
|
196
|
-
(subs
|
|
215
|
+
(subs.filterNot { it.isAcknowledged } + inapp).forEach { emit(toMap(it)) }
|
|
197
216
|
}
|
|
198
217
|
}
|
|
199
218
|
}
|
|
@@ -210,7 +229,7 @@ class RevnixStoreModule(context: ReactApplicationContext) :
|
|
|
210
229
|
BillingResponseCode.ITEM_ALREADY_OWNED -> queryPurchases(ProductType.SUBS) { subs ->
|
|
211
230
|
queryPurchases(ProductType.INAPP) { inapp ->
|
|
212
231
|
val owned = (subs + inapp).firstOrNull { waiting.productId in it.products }
|
|
213
|
-
if (owned != null) waiting.promise.resolve(toMap(owned))
|
|
232
|
+
if (owned != null) waiting.promise.resolve(toMap(owned).apply { putBoolean("alreadyOwned", true) })
|
|
214
233
|
else waiting.promise.reject("store_error", "${result.responseCode} ${result.debugMessage}")
|
|
215
234
|
}
|
|
216
235
|
}
|
|
@@ -274,6 +293,7 @@ class RevnixStoreModule(context: ReactApplicationContext) :
|
|
|
274
293
|
putString("transactionId", purchase.purchaseToken)
|
|
275
294
|
putString("token", purchase.purchaseToken)
|
|
276
295
|
putDouble("occurredAt", purchase.purchaseTime.toDouble())
|
|
296
|
+
putBoolean("acknowledged", purchase.isAcknowledged)
|
|
277
297
|
}
|
|
278
298
|
}
|
|
279
299
|
|
package/dist/client.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare class RevnixClient {
|
|
|
27
27
|
private readonly onDeferredDeepLink?;
|
|
28
28
|
private readonly onAttribution?;
|
|
29
29
|
private readonly deviceIntegrity?;
|
|
30
|
+
private readonly consumableProductIds;
|
|
30
31
|
private integrityRequest;
|
|
31
32
|
private deferredDeepLinkClaimed;
|
|
32
33
|
private landingUrl;
|
|
@@ -106,6 +107,7 @@ export declare class RevnixClient {
|
|
|
106
107
|
/** Re-registers every purchase the store still holds for this device and
|
|
107
108
|
* resolves how many registered. Per-purchase failures go to onDiagnostic. */
|
|
108
109
|
restore(): Promise<number>;
|
|
110
|
+
private isConsumable;
|
|
109
111
|
private requireStore;
|
|
110
112
|
private registerStoreTransaction;
|
|
111
113
|
private postPurchase;
|
package/dist/client.js
CHANGED
|
@@ -97,6 +97,32 @@ const pendingKey = (p) => `${p.source}:${p.token}:${p.transactionId}`;
|
|
|
97
97
|
// revoked key, blocked transfer) would be retried every launch forever;
|
|
98
98
|
// unknown non-Revnix errors count as transient to err on the keeping side.
|
|
99
99
|
const worthQueueing = (err) => !(err instanceof errors_js_1.RevnixError) || err.isRetryable;
|
|
100
|
+
const ATTRIBUTION_QUERY_PARAMS = new Set([
|
|
101
|
+
"utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content", "utm_id", "campaign_id",
|
|
102
|
+
"pid", "source", "af_channel", "medium", "c", "campaign", "term", "af_ad",
|
|
103
|
+
"content", "gclid", "gbraid", "wbraid", "ttclid", "sccid", "twclid",
|
|
104
|
+
"revnix_link", "web_id",
|
|
105
|
+
]);
|
|
106
|
+
function queryParamName(pair) {
|
|
107
|
+
const raw = pair.split("=")[0] ?? "";
|
|
108
|
+
try {
|
|
109
|
+
return decodeURIComponent(raw.replace(/\+/g, " ")).toLowerCase();
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return raw.toLowerCase();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function attributionOnlyUrl(rawUrl) {
|
|
116
|
+
const url = rawUrl.trim();
|
|
117
|
+
const parts = /^([a-z][a-z0-9+.-]*:)(\/\/[^/?#]*)?([^?#]*)(?:\?([^#]*))?/i.exec(url);
|
|
118
|
+
if (!parts)
|
|
119
|
+
return url.split(/[?#]/)[0] ?? "";
|
|
120
|
+
const [, scheme, authority = "", path, query = ""] = parts;
|
|
121
|
+
const kept = query
|
|
122
|
+
.split("&")
|
|
123
|
+
.filter((pair) => ATTRIBUTION_QUERY_PARAMS.has(queryParamName(pair)));
|
|
124
|
+
return `${scheme}${authority.replace(/^\/\/.*@/, "//")}${path}${kept.length > 0 ? `?${kept.join("&")}` : ""}`;
|
|
125
|
+
}
|
|
100
126
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
101
127
|
// REV-199 (CTO-R2 M24): ±20% on every scheduled delay.
|
|
102
128
|
const jittered = (ms) => ms * (0.8 + Math.random() * 0.4);
|
|
@@ -207,6 +233,7 @@ class RevnixClient {
|
|
|
207
233
|
this.onDeferredDeepLink = config.onDeferredDeepLink;
|
|
208
234
|
this.onAttribution = config.onAttribution;
|
|
209
235
|
this.deviceIntegrity = config.deviceIntegrity;
|
|
236
|
+
this.consumableProductIds = config.consumableProductIds ?? [];
|
|
210
237
|
this.readYourWritesDelaysMs =
|
|
211
238
|
config.readYourWritesDelaysMs ?? [250, 500, 1000, 2000];
|
|
212
239
|
this.customerIdPromise = this.loadCustomerId(config.customerId);
|
|
@@ -217,7 +244,11 @@ class RevnixClient {
|
|
|
217
244
|
this.reportAdvertisingId(config.advertisingId);
|
|
218
245
|
const store = (0, store_js_1.getStoreAdapter)();
|
|
219
246
|
if (store) {
|
|
220
|
-
this.stopStoreObserver = store.observe((tx) =>
|
|
247
|
+
this.stopStoreObserver = store.observe((tx) => {
|
|
248
|
+
if (tx.acknowledged && !this.isConsumable(tx))
|
|
249
|
+
return;
|
|
250
|
+
void this.registerStoreTransaction(store, tx).catch((err) => this.diag("storeObserver", err));
|
|
251
|
+
});
|
|
221
252
|
}
|
|
222
253
|
// REV-157: warm the placement cache at launch, so the first paywall of the
|
|
223
254
|
// session can render offline. Fire-and-forget; a failed key just stays on
|
|
@@ -378,6 +409,10 @@ class RevnixClient {
|
|
|
378
409
|
const tx = await store.purchase(productId, { customerId });
|
|
379
410
|
if (!tx)
|
|
380
411
|
return null;
|
|
412
|
+
if (tx.alreadyOwned && this.isConsumable(tx)) {
|
|
413
|
+
await this.registerStoreTransaction(store, tx, opts);
|
|
414
|
+
throw new errors_js_1.PurchaseAlreadyOwnedError(`Revnix: an earlier purchase of ${productId} was still owned and has been registered; buy again`);
|
|
415
|
+
}
|
|
381
416
|
return this.registerStoreTransaction(store, tx, opts);
|
|
382
417
|
}
|
|
383
418
|
/** Re-registers every purchase the store still holds for this device and
|
|
@@ -396,6 +431,9 @@ class RevnixClient {
|
|
|
396
431
|
}
|
|
397
432
|
return restored;
|
|
398
433
|
}
|
|
434
|
+
isConsumable(tx) {
|
|
435
|
+
return tx.store === "google" && this.consumableProductIds.includes(tx.productId);
|
|
436
|
+
}
|
|
399
437
|
requireStore() {
|
|
400
438
|
const store = (0, store_js_1.getStoreAdapter)();
|
|
401
439
|
if (!store) {
|
|
@@ -404,8 +442,8 @@ class RevnixClient {
|
|
|
404
442
|
return store;
|
|
405
443
|
}
|
|
406
444
|
async registerStoreTransaction(store, tx, opts, finish = true) {
|
|
407
|
-
const finishTx = () => finish
|
|
408
|
-
? store.finish(tx.transactionId).catch((err) => this.diag("storeFinish", err))
|
|
445
|
+
const finishTx = (consume = false) => finish
|
|
446
|
+
? store.finish(tx.transactionId, consume).catch((err) => this.diag("storeFinish", err))
|
|
409
447
|
: undefined;
|
|
410
448
|
try {
|
|
411
449
|
const result = await this.registerPurchase({
|
|
@@ -418,7 +456,7 @@ class RevnixClient {
|
|
|
418
456
|
signedTransactionInfo: tx.signedTransactionInfo,
|
|
419
457
|
...opts,
|
|
420
458
|
});
|
|
421
|
-
await finishTx();
|
|
459
|
+
await finishTx(this.isConsumable(tx));
|
|
422
460
|
return result;
|
|
423
461
|
}
|
|
424
462
|
catch (err) {
|
|
@@ -1099,7 +1137,7 @@ class RevnixClient {
|
|
|
1099
1137
|
return;
|
|
1100
1138
|
}
|
|
1101
1139
|
void this.rememberDeepLink(url);
|
|
1102
|
-
const extra = { url: url.slice(0, 1024) };
|
|
1140
|
+
const extra = { url: attributionOnlyUrl(url).slice(0, 1024) };
|
|
1103
1141
|
const batch = this.launchBatch;
|
|
1104
1142
|
if (batch === null) {
|
|
1105
1143
|
this.fireImplicit("deeplink_open", extra);
|
package/dist/errors.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export declare class RevnixError extends Error {
|
|
|
11
11
|
export declare class PurchaseBlockedError extends RevnixError {
|
|
12
12
|
constructor(message: string);
|
|
13
13
|
}
|
|
14
|
+
/** purchase() found an earlier Google Play consumable of this product still
|
|
15
|
+
* owned. It was registered and consumed instead of charging again; ask the
|
|
16
|
+
* customer to buy again. */
|
|
17
|
+
export declare class PurchaseAlreadyOwnedError extends Error {
|
|
18
|
+
constructor(message: string);
|
|
19
|
+
}
|
|
14
20
|
/** C7: the request didn't complete within the configured timeout (default
|
|
15
21
|
* 10s). `fetch` has no default timeout on React Native / browsers, so without
|
|
16
22
|
* this a slow or dead backend would hang the app until the OS TCP timeout. */
|
package/dist/errors.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RevnixRateLimitError = exports.RevnixAuthError = exports.RevnixNetworkError = exports.RevnixTimeoutError = exports.PurchaseBlockedError = exports.RevnixError = void 0;
|
|
3
|
+
exports.RevnixRateLimitError = exports.RevnixAuthError = exports.RevnixNetworkError = exports.RevnixTimeoutError = exports.PurchaseAlreadyOwnedError = exports.PurchaseBlockedError = exports.RevnixError = void 0;
|
|
4
4
|
class RevnixError extends Error {
|
|
5
5
|
constructor(status, message) {
|
|
6
6
|
super(message);
|
|
@@ -24,6 +24,16 @@ class PurchaseBlockedError extends RevnixError {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
exports.PurchaseBlockedError = PurchaseBlockedError;
|
|
27
|
+
/** purchase() found an earlier Google Play consumable of this product still
|
|
28
|
+
* owned. It was registered and consumed instead of charging again; ask the
|
|
29
|
+
* customer to buy again. */
|
|
30
|
+
class PurchaseAlreadyOwnedError extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "PurchaseAlreadyOwnedError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.PurchaseAlreadyOwnedError = PurchaseAlreadyOwnedError;
|
|
27
37
|
/** C7: the request didn't complete within the configured timeout (default
|
|
28
38
|
* 10s). `fetch` has no default timeout on React Native / browsers, so without
|
|
29
39
|
* this a slow or dead backend would hang the app until the OS TCP timeout. */
|
package/dist/esm/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PurchaseBlockedError, RevnixAuthError, RevnixError, RevnixNetworkError, RevnixRateLimitError, RevnixTimeoutError, } from "./errors.js";
|
|
1
|
+
import { PurchaseAlreadyOwnedError, PurchaseBlockedError, RevnixAuthError, RevnixError, RevnixNetworkError, RevnixRateLimitError, RevnixTimeoutError, } from "./errors.js";
|
|
2
2
|
import { generateAnonymousId, generateEventId, generateViewId, resolveStorage, } from "./storage.js";
|
|
3
3
|
import { DEVICE_HEADER, INSTALLED_AT_KEY, compactPayload, detectDeviceFacts, encodeDeviceHeader, } from "./device.js";
|
|
4
4
|
import { SDK_VERSION } from "./version.js";
|
|
@@ -93,6 +93,32 @@ const pendingKey = (p) => `${p.source}:${p.token}:${p.transactionId}`;
|
|
|
93
93
|
// revoked key, blocked transfer) would be retried every launch forever;
|
|
94
94
|
// unknown non-Revnix errors count as transient to err on the keeping side.
|
|
95
95
|
const worthQueueing = (err) => !(err instanceof RevnixError) || err.isRetryable;
|
|
96
|
+
const ATTRIBUTION_QUERY_PARAMS = new Set([
|
|
97
|
+
"utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content", "utm_id", "campaign_id",
|
|
98
|
+
"pid", "source", "af_channel", "medium", "c", "campaign", "term", "af_ad",
|
|
99
|
+
"content", "gclid", "gbraid", "wbraid", "ttclid", "sccid", "twclid",
|
|
100
|
+
"revnix_link", "web_id",
|
|
101
|
+
]);
|
|
102
|
+
function queryParamName(pair) {
|
|
103
|
+
const raw = pair.split("=")[0] ?? "";
|
|
104
|
+
try {
|
|
105
|
+
return decodeURIComponent(raw.replace(/\+/g, " ")).toLowerCase();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return raw.toLowerCase();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function attributionOnlyUrl(rawUrl) {
|
|
112
|
+
const url = rawUrl.trim();
|
|
113
|
+
const parts = /^([a-z][a-z0-9+.-]*:)(\/\/[^/?#]*)?([^?#]*)(?:\?([^#]*))?/i.exec(url);
|
|
114
|
+
if (!parts)
|
|
115
|
+
return url.split(/[?#]/)[0] ?? "";
|
|
116
|
+
const [, scheme, authority = "", path, query = ""] = parts;
|
|
117
|
+
const kept = query
|
|
118
|
+
.split("&")
|
|
119
|
+
.filter((pair) => ATTRIBUTION_QUERY_PARAMS.has(queryParamName(pair)));
|
|
120
|
+
return `${scheme}${authority.replace(/^\/\/.*@/, "//")}${path}${kept.length > 0 ? `?${kept.join("&")}` : ""}`;
|
|
121
|
+
}
|
|
96
122
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
97
123
|
// REV-199 (CTO-R2 M24): ±20% on every scheduled delay.
|
|
98
124
|
const jittered = (ms) => ms * (0.8 + Math.random() * 0.4);
|
|
@@ -203,6 +229,7 @@ export class RevnixClient {
|
|
|
203
229
|
this.onDeferredDeepLink = config.onDeferredDeepLink;
|
|
204
230
|
this.onAttribution = config.onAttribution;
|
|
205
231
|
this.deviceIntegrity = config.deviceIntegrity;
|
|
232
|
+
this.consumableProductIds = config.consumableProductIds ?? [];
|
|
206
233
|
this.readYourWritesDelaysMs =
|
|
207
234
|
config.readYourWritesDelaysMs ?? [250, 500, 1000, 2000];
|
|
208
235
|
this.customerIdPromise = this.loadCustomerId(config.customerId);
|
|
@@ -213,7 +240,11 @@ export class RevnixClient {
|
|
|
213
240
|
this.reportAdvertisingId(config.advertisingId);
|
|
214
241
|
const store = getStoreAdapter();
|
|
215
242
|
if (store) {
|
|
216
|
-
this.stopStoreObserver = store.observe((tx) =>
|
|
243
|
+
this.stopStoreObserver = store.observe((tx) => {
|
|
244
|
+
if (tx.acknowledged && !this.isConsumable(tx))
|
|
245
|
+
return;
|
|
246
|
+
void this.registerStoreTransaction(store, tx).catch((err) => this.diag("storeObserver", err));
|
|
247
|
+
});
|
|
217
248
|
}
|
|
218
249
|
// REV-157: warm the placement cache at launch, so the first paywall of the
|
|
219
250
|
// session can render offline. Fire-and-forget; a failed key just stays on
|
|
@@ -374,6 +405,10 @@ export class RevnixClient {
|
|
|
374
405
|
const tx = await store.purchase(productId, { customerId });
|
|
375
406
|
if (!tx)
|
|
376
407
|
return null;
|
|
408
|
+
if (tx.alreadyOwned && this.isConsumable(tx)) {
|
|
409
|
+
await this.registerStoreTransaction(store, tx, opts);
|
|
410
|
+
throw new PurchaseAlreadyOwnedError(`Revnix: an earlier purchase of ${productId} was still owned and has been registered; buy again`);
|
|
411
|
+
}
|
|
377
412
|
return this.registerStoreTransaction(store, tx, opts);
|
|
378
413
|
}
|
|
379
414
|
/** Re-registers every purchase the store still holds for this device and
|
|
@@ -392,6 +427,9 @@ export class RevnixClient {
|
|
|
392
427
|
}
|
|
393
428
|
return restored;
|
|
394
429
|
}
|
|
430
|
+
isConsumable(tx) {
|
|
431
|
+
return tx.store === "google" && this.consumableProductIds.includes(tx.productId);
|
|
432
|
+
}
|
|
395
433
|
requireStore() {
|
|
396
434
|
const store = getStoreAdapter();
|
|
397
435
|
if (!store) {
|
|
@@ -400,8 +438,8 @@ export class RevnixClient {
|
|
|
400
438
|
return store;
|
|
401
439
|
}
|
|
402
440
|
async registerStoreTransaction(store, tx, opts, finish = true) {
|
|
403
|
-
const finishTx = () => finish
|
|
404
|
-
? store.finish(tx.transactionId).catch((err) => this.diag("storeFinish", err))
|
|
441
|
+
const finishTx = (consume = false) => finish
|
|
442
|
+
? store.finish(tx.transactionId, consume).catch((err) => this.diag("storeFinish", err))
|
|
405
443
|
: undefined;
|
|
406
444
|
try {
|
|
407
445
|
const result = await this.registerPurchase({
|
|
@@ -414,7 +452,7 @@ export class RevnixClient {
|
|
|
414
452
|
signedTransactionInfo: tx.signedTransactionInfo,
|
|
415
453
|
...opts,
|
|
416
454
|
});
|
|
417
|
-
await finishTx();
|
|
455
|
+
await finishTx(this.isConsumable(tx));
|
|
418
456
|
return result;
|
|
419
457
|
}
|
|
420
458
|
catch (err) {
|
|
@@ -1095,7 +1133,7 @@ export class RevnixClient {
|
|
|
1095
1133
|
return;
|
|
1096
1134
|
}
|
|
1097
1135
|
void this.rememberDeepLink(url);
|
|
1098
|
-
const extra = { url: url.slice(0, 1024) };
|
|
1136
|
+
const extra = { url: attributionOnlyUrl(url).slice(0, 1024) };
|
|
1099
1137
|
const batch = this.launchBatch;
|
|
1100
1138
|
if (batch === null) {
|
|
1101
1139
|
this.fireImplicit("deeplink_open", extra);
|
package/dist/esm/errors.js
CHANGED
|
@@ -19,6 +19,15 @@ export class PurchaseBlockedError extends RevnixError {
|
|
|
19
19
|
this.name = "PurchaseBlockedError";
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
/** purchase() found an earlier Google Play consumable of this product still
|
|
23
|
+
* owned. It was registered and consumed instead of charging again; ask the
|
|
24
|
+
* customer to buy again. */
|
|
25
|
+
export class PurchaseAlreadyOwnedError extends Error {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "PurchaseAlreadyOwnedError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
22
31
|
/** C7: the request didn't complete within the configured timeout (default
|
|
23
32
|
* 10s). `fetch` has no default timeout on React Native / browsers, so without
|
|
24
33
|
* this a slow or dead backend would hang the app until the OS TCP timeout. */
|
package/dist/esm/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RevnixClient } from "./client.js";
|
|
2
2
|
import { setLocale } from "./blocks/locale.js";
|
|
3
3
|
export { RevnixClient } from "./client.js";
|
|
4
|
-
export { PurchaseBlockedError, RevnixAuthError, RevnixError, RevnixNetworkError, RevnixRateLimitError, RevnixTimeoutError, } from "./errors.js";
|
|
4
|
+
export { PurchaseAlreadyOwnedError, PurchaseBlockedError, RevnixAuthError, RevnixError, RevnixNetworkError, RevnixRateLimitError, RevnixTimeoutError, } from "./errors.js";
|
|
5
5
|
export { purchaseInputFromIap } from "./iap.js";
|
|
6
6
|
export { setStoreAdapter, } from "./store.js";
|
|
7
7
|
export { memoryStorage, generateAnonymousId } from "./storage.js";
|
package/dist/esm/store/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export const RevnixStore = native
|
|
|
6
6
|
? {
|
|
7
7
|
purchase: (productId, { customerId }) => native.purchase(productId, customerId),
|
|
8
8
|
restore: () => native.restore(),
|
|
9
|
-
finish: (transactionId) => native.finish(transactionId),
|
|
9
|
+
finish: (transactionId, consume) => native.finish(transactionId, consume === true),
|
|
10
10
|
observe(onTransaction) {
|
|
11
11
|
const sub = new NativeEventEmitter(NativeModules.RevnixStore).addListener("revnixStoreTransaction", (tx) => onTransaction(tx));
|
|
12
12
|
return () => sub.remove();
|
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.9.0";
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { RevnixClient } from "./client.js";
|
|
|
2
2
|
import { setLocale } from "./blocks/locale.js";
|
|
3
3
|
import type { RevnixConfig } from "./types.js";
|
|
4
4
|
export { RevnixClient } from "./client.js";
|
|
5
|
-
export { PurchaseBlockedError, RevnixAuthError, RevnixError, RevnixNetworkError, RevnixRateLimitError, RevnixTimeoutError, } from "./errors.js";
|
|
5
|
+
export { PurchaseAlreadyOwnedError, PurchaseBlockedError, RevnixAuthError, RevnixError, RevnixNetworkError, RevnixRateLimitError, RevnixTimeoutError, } from "./errors.js";
|
|
6
6
|
export { purchaseInputFromIap, type IapPurchase } from "./iap.js";
|
|
7
7
|
export { setStoreAdapter, type StoreAdapter, type StoreTransaction, } from "./store.js";
|
|
8
8
|
export { memoryStorage, generateAnonymousId } from "./storage.js";
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
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.setStoreAdapter = 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.PurchaseAlreadyOwnedError = 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");
|
|
7
7
|
Object.defineProperty(exports, "RevnixClient", { enumerable: true, get: function () { return client_js_2.RevnixClient; } });
|
|
8
8
|
var errors_js_1 = require("./errors.js");
|
|
9
|
+
Object.defineProperty(exports, "PurchaseAlreadyOwnedError", { enumerable: true, get: function () { return errors_js_1.PurchaseAlreadyOwnedError; } });
|
|
9
10
|
Object.defineProperty(exports, "PurchaseBlockedError", { enumerable: true, get: function () { return errors_js_1.PurchaseBlockedError; } });
|
|
10
11
|
Object.defineProperty(exports, "RevnixAuthError", { enumerable: true, get: function () { return errors_js_1.RevnixAuthError; } });
|
|
11
12
|
Object.defineProperty(exports, "RevnixError", { enumerable: true, get: function () { return errors_js_1.RevnixError; } });
|
package/dist/store/index.js
CHANGED
|
@@ -9,7 +9,7 @@ exports.RevnixStore = native
|
|
|
9
9
|
? {
|
|
10
10
|
purchase: (productId, { customerId }) => native.purchase(productId, customerId),
|
|
11
11
|
restore: () => native.restore(),
|
|
12
|
-
finish: (transactionId) => native.finish(transactionId),
|
|
12
|
+
finish: (transactionId, consume) => native.finish(transactionId, consume === true),
|
|
13
13
|
observe(onTransaction) {
|
|
14
14
|
const sub = new react_native_1.NativeEventEmitter(react_native_1.NativeModules.RevnixStore).addListener("revnixStoreTransaction", (tx) => onTransaction(tx));
|
|
15
15
|
return () => sub.remove();
|
package/dist/store.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export interface StoreTransaction {
|
|
|
12
12
|
expiresAt?: number;
|
|
13
13
|
/** Apple only: the StoreKit 2 JWS. */
|
|
14
14
|
signedTransactionInfo?: string;
|
|
15
|
+
/** Google only: already acknowledged in Play. */
|
|
16
|
+
acknowledged?: boolean;
|
|
17
|
+
/** Google only: purchase() answered with an earlier purchase Play reported
|
|
18
|
+
* as already owned, not a new charge. */
|
|
19
|
+
alreadyOwned?: boolean;
|
|
15
20
|
}
|
|
16
21
|
export interface StoreAdapter {
|
|
17
22
|
/** Resolves null when the customer cancelled or the purchase is pending. */
|
|
@@ -19,7 +24,8 @@ export interface StoreAdapter {
|
|
|
19
24
|
customerId: string;
|
|
20
25
|
}): Promise<StoreTransaction | null>;
|
|
21
26
|
restore(): Promise<StoreTransaction[]>;
|
|
22
|
-
|
|
27
|
+
/** `consume`: Google only, consume instead of acknowledge. */
|
|
28
|
+
finish(transactionId: string, consume?: boolean): Promise<void>;
|
|
23
29
|
/** Transactions arriving outside purchase(): renewals, other devices, Ask
|
|
24
30
|
* to Buy, refunds. Returns an unsubscribe. */
|
|
25
31
|
observe(onTransaction: (tx: StoreTransaction) => void): () => void;
|
package/dist/types.d.ts
CHANGED
|
@@ -149,6 +149,9 @@ export interface RevnixConfig {
|
|
|
149
149
|
* read. A null answer deletes a previously stored `gps_adid`; a
|
|
150
150
|
* rejection changes nothing and reports `op: "advertisingId"`. */
|
|
151
151
|
advertisingId?: () => Promise<string | null | undefined>;
|
|
152
|
+
/** Google Play product ids sold as consumables. A registered purchase of
|
|
153
|
+
* one is consumed instead of acknowledged so it can be bought again. */
|
|
154
|
+
consumableProductIds?: string[];
|
|
152
155
|
}
|
|
153
156
|
/** Device integrity evidence for `POST /v1/installs` and `/v1/attribution`.
|
|
154
157
|
* iOS: an App Attest key id and its attestation (standard base64) over
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.
|
|
1
|
+
export declare const SDK_VERSION = "1.9.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.9.0";
|
package/ios/RevnixStore.m
CHANGED
|
@@ -12,6 +12,7 @@ RCT_EXTERN_METHOD(restore:(RCTPromiseResolveBlock)resolve
|
|
|
12
12
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
13
13
|
|
|
14
14
|
RCT_EXTERN_METHOD(finish:(NSString *)transactionId
|
|
15
|
+
consume:(BOOL)consume
|
|
15
16
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
16
17
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
17
18
|
|
package/ios/RevnixStore.swift
CHANGED
|
@@ -20,7 +20,7 @@ class RevnixStore: RCTEventEmitter {
|
|
|
20
20
|
override func startObserving() {
|
|
21
21
|
updates?.cancel()
|
|
22
22
|
updates = Task { [weak self] in
|
|
23
|
-
for await update in Transaction.updates
|
|
23
|
+
for await update in Transaction.updates {
|
|
24
24
|
self?.sendEvent(withName: "revnixStoreTransaction", body: RevnixStore.payload(update))
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -72,9 +72,10 @@ class RevnixStore: RCTEventEmitter {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
@objc(finish:resolver:rejecter:)
|
|
75
|
+
@objc(finish:consume:resolver:rejecter:)
|
|
76
76
|
func finish(
|
|
77
77
|
_ transactionId: String,
|
|
78
|
+
consume _: Bool,
|
|
78
79
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
79
80
|
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
80
81
|
) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-revnix",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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": {
|
package/src/client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
PurchaseAlreadyOwnedError,
|
|
2
3
|
PurchaseBlockedError,
|
|
3
4
|
RevnixAuthError,
|
|
4
5
|
RevnixError,
|
|
@@ -193,6 +194,33 @@ const pendingKey = (p: RegisterPurchaseInput) =>
|
|
|
193
194
|
const worthQueueing = (err: unknown): boolean =>
|
|
194
195
|
!(err instanceof RevnixError) || err.isRetryable;
|
|
195
196
|
|
|
197
|
+
const ATTRIBUTION_QUERY_PARAMS = new Set([
|
|
198
|
+
"utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content", "utm_id", "campaign_id",
|
|
199
|
+
"pid", "source", "af_channel", "medium", "c", "campaign", "term", "af_ad",
|
|
200
|
+
"content", "gclid", "gbraid", "wbraid", "ttclid", "sccid", "twclid",
|
|
201
|
+
"revnix_link", "web_id",
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
function queryParamName(pair: string): string {
|
|
205
|
+
const raw = pair.split("=")[0] ?? "";
|
|
206
|
+
try {
|
|
207
|
+
return decodeURIComponent(raw.replace(/\+/g, " ")).toLowerCase();
|
|
208
|
+
} catch {
|
|
209
|
+
return raw.toLowerCase();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function attributionOnlyUrl(rawUrl: string): string {
|
|
214
|
+
const url = rawUrl.trim();
|
|
215
|
+
const parts = /^([a-z][a-z0-9+.-]*:)(\/\/[^/?#]*)?([^?#]*)(?:\?([^#]*))?/i.exec(url);
|
|
216
|
+
if (!parts) return url.split(/[?#]/)[0] ?? "";
|
|
217
|
+
const [, scheme, authority = "", path, query = ""] = parts;
|
|
218
|
+
const kept = query
|
|
219
|
+
.split("&")
|
|
220
|
+
.filter((pair) => ATTRIBUTION_QUERY_PARAMS.has(queryParamName(pair)));
|
|
221
|
+
return `${scheme}${authority.replace(/^\/\/.*@/, "//")}${path}${kept.length > 0 ? `?${kept.join("&")}` : ""}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
196
224
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
197
225
|
|
|
198
226
|
// REV-199 (CTO-R2 M24): ±20% on every scheduled delay.
|
|
@@ -263,6 +291,7 @@ export class RevnixClient {
|
|
|
263
291
|
private readonly onDeferredDeepLink?: RevnixConfig["onDeferredDeepLink"];
|
|
264
292
|
private readonly onAttribution?: RevnixConfig["onAttribution"];
|
|
265
293
|
private readonly deviceIntegrity?: RevnixConfig["deviceIntegrity"];
|
|
294
|
+
private readonly consumableProductIds: string[];
|
|
266
295
|
private integrityRequest: {
|
|
267
296
|
customerId: string;
|
|
268
297
|
body: Promise<{ integrity?: IntegrityEvidence }>;
|
|
@@ -340,6 +369,7 @@ export class RevnixClient {
|
|
|
340
369
|
this.onDeferredDeepLink = config.onDeferredDeepLink;
|
|
341
370
|
this.onAttribution = config.onAttribution;
|
|
342
371
|
this.deviceIntegrity = config.deviceIntegrity;
|
|
372
|
+
this.consumableProductIds = config.consumableProductIds ?? [];
|
|
343
373
|
this.readYourWritesDelaysMs =
|
|
344
374
|
config.readYourWritesDelaysMs ?? [250, 500, 1000, 2000];
|
|
345
375
|
this.customerIdPromise = this.loadCustomerId(config.customerId);
|
|
@@ -351,11 +381,12 @@ export class RevnixClient {
|
|
|
351
381
|
if (config.advertisingId) this.reportAdvertisingId(config.advertisingId);
|
|
352
382
|
const store = getStoreAdapter();
|
|
353
383
|
if (store) {
|
|
354
|
-
this.stopStoreObserver = store.observe((tx) =>
|
|
384
|
+
this.stopStoreObserver = store.observe((tx) => {
|
|
385
|
+
if (tx.acknowledged && !this.isConsumable(tx)) return;
|
|
355
386
|
void this.registerStoreTransaction(store, tx).catch((err) =>
|
|
356
387
|
this.diag("storeObserver", err)
|
|
357
|
-
)
|
|
358
|
-
);
|
|
388
|
+
);
|
|
389
|
+
});
|
|
359
390
|
}
|
|
360
391
|
// REV-157: warm the placement cache at launch, so the first paywall of the
|
|
361
392
|
// session can render offline. Fire-and-forget; a failed key just stays on
|
|
@@ -547,6 +578,12 @@ export class RevnixClient {
|
|
|
547
578
|
const customerId = await this.customerIdPromise;
|
|
548
579
|
const tx = await store.purchase(productId, { customerId });
|
|
549
580
|
if (!tx) return null;
|
|
581
|
+
if (tx.alreadyOwned && this.isConsumable(tx)) {
|
|
582
|
+
await this.registerStoreTransaction(store, tx, opts);
|
|
583
|
+
throw new PurchaseAlreadyOwnedError(
|
|
584
|
+
`Revnix: an earlier purchase of ${productId} was still owned and has been registered; buy again`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
550
587
|
return this.registerStoreTransaction(store, tx, opts);
|
|
551
588
|
}
|
|
552
589
|
|
|
@@ -566,6 +603,10 @@ export class RevnixClient {
|
|
|
566
603
|
return restored;
|
|
567
604
|
}
|
|
568
605
|
|
|
606
|
+
private isConsumable(tx: StoreTransaction): boolean {
|
|
607
|
+
return tx.store === "google" && this.consumableProductIds.includes(tx.productId);
|
|
608
|
+
}
|
|
609
|
+
|
|
569
610
|
private requireStore(): StoreAdapter {
|
|
570
611
|
const store = getStoreAdapter();
|
|
571
612
|
if (!store) {
|
|
@@ -582,9 +623,9 @@ export class RevnixClient {
|
|
|
582
623
|
opts?: Pick<RegisterPurchaseInput, "viewId" | "paywallId" | "placementKey">,
|
|
583
624
|
finish = true
|
|
584
625
|
): Promise<RegisterPurchaseResult> {
|
|
585
|
-
const finishTx = () =>
|
|
626
|
+
const finishTx = (consume = false) =>
|
|
586
627
|
finish
|
|
587
|
-
? store.finish(tx.transactionId).catch((err) => this.diag("storeFinish", err))
|
|
628
|
+
? store.finish(tx.transactionId, consume).catch((err) => this.diag("storeFinish", err))
|
|
588
629
|
: undefined;
|
|
589
630
|
try {
|
|
590
631
|
const result = await this.registerPurchase({
|
|
@@ -597,7 +638,7 @@ export class RevnixClient {
|
|
|
597
638
|
signedTransactionInfo: tx.signedTransactionInfo,
|
|
598
639
|
...opts,
|
|
599
640
|
});
|
|
600
|
-
await finishTx();
|
|
641
|
+
await finishTx(this.isConsumable(tx));
|
|
601
642
|
return result;
|
|
602
643
|
} catch (err) {
|
|
603
644
|
if (!(err instanceof PurchaseBlockedError)) await finishTx();
|
|
@@ -1400,7 +1441,7 @@ export class RevnixClient {
|
|
|
1400
1441
|
return;
|
|
1401
1442
|
}
|
|
1402
1443
|
void this.rememberDeepLink(url);
|
|
1403
|
-
const extra = { url: url.slice(0, 1024) };
|
|
1444
|
+
const extra = { url: attributionOnlyUrl(url).slice(0, 1024) };
|
|
1404
1445
|
const batch = this.launchBatch;
|
|
1405
1446
|
if (batch === null) {
|
|
1406
1447
|
this.fireImplicit("deeplink_open", extra);
|
package/src/errors.ts
CHANGED
|
@@ -24,6 +24,16 @@ export class PurchaseBlockedError extends RevnixError {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** purchase() found an earlier Google Play consumable of this product still
|
|
28
|
+
* owned. It was registered and consumed instead of charging again; ask the
|
|
29
|
+
* customer to buy again. */
|
|
30
|
+
export class PurchaseAlreadyOwnedError extends Error {
|
|
31
|
+
constructor(message: string) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "PurchaseAlreadyOwnedError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
/** C7: the request didn't complete within the configured timeout (default
|
|
28
38
|
* 10s). `fetch` has no default timeout on React Native / browsers, so without
|
|
29
39
|
* this a slow or dead backend would hang the app until the OS TCP timeout. */
|
package/src/index.ts
CHANGED
package/src/store/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { IntegrityEvidence, RevnixConfig } from "../types.js";
|
|
|
10
10
|
interface NativeRevnixStore {
|
|
11
11
|
purchase(productId: string, customerId: string): Promise<StoreTransaction | null>;
|
|
12
12
|
restore(): Promise<StoreTransaction[]>;
|
|
13
|
-
finish(transactionId: string): Promise<void>;
|
|
13
|
+
finish(transactionId: string, consume: boolean): Promise<void>;
|
|
14
14
|
deviceIntegrity?(customerId: string): Promise<IntegrityEvidence | null>;
|
|
15
15
|
advertisingId?(): Promise<string | null>;
|
|
16
16
|
}
|
|
@@ -28,7 +28,7 @@ export const RevnixStore: StoreAdapter = native
|
|
|
28
28
|
? {
|
|
29
29
|
purchase: (productId, { customerId }) => native.purchase(productId, customerId),
|
|
30
30
|
restore: () => native.restore(),
|
|
31
|
-
finish: (transactionId) => native.finish(transactionId),
|
|
31
|
+
finish: (transactionId, consume) => native.finish(transactionId, consume === true),
|
|
32
32
|
observe(onTransaction) {
|
|
33
33
|
const sub = new NativeEventEmitter(NativeModules.RevnixStore).addListener(
|
|
34
34
|
"revnixStoreTransaction",
|
package/src/store.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface StoreTransaction {
|
|
|
13
13
|
expiresAt?: number;
|
|
14
14
|
/** Apple only: the StoreKit 2 JWS. */
|
|
15
15
|
signedTransactionInfo?: string;
|
|
16
|
+
/** Google only: already acknowledged in Play. */
|
|
17
|
+
acknowledged?: boolean;
|
|
18
|
+
/** Google only: purchase() answered with an earlier purchase Play reported
|
|
19
|
+
* as already owned, not a new charge. */
|
|
20
|
+
alreadyOwned?: boolean;
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
export interface StoreAdapter {
|
|
@@ -22,7 +27,8 @@ export interface StoreAdapter {
|
|
|
22
27
|
opts: { customerId: string }
|
|
23
28
|
): Promise<StoreTransaction | null>;
|
|
24
29
|
restore(): Promise<StoreTransaction[]>;
|
|
25
|
-
|
|
30
|
+
/** `consume`: Google only, consume instead of acknowledge. */
|
|
31
|
+
finish(transactionId: string, consume?: boolean): Promise<void>;
|
|
26
32
|
/** Transactions arriving outside purchase(): renewals, other devices, Ask
|
|
27
33
|
* to Buy, refunds. Returns an unsubscribe. */
|
|
28
34
|
observe(onTransaction: (tx: StoreTransaction) => void): () => void;
|
package/src/types.ts
CHANGED
|
@@ -161,6 +161,9 @@ export interface RevnixConfig {
|
|
|
161
161
|
* read. A null answer deletes a previously stored `gps_adid`; a
|
|
162
162
|
* rejection changes nothing and reports `op: "advertisingId"`. */
|
|
163
163
|
advertisingId?: () => Promise<string | null | undefined>;
|
|
164
|
+
/** Google Play product ids sold as consumables. A registered purchase of
|
|
165
|
+
* one is consumed instead of acknowledged so it can be bought again. */
|
|
166
|
+
consumableProductIds?: string[];
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
/** Device integrity evidence for `POST /v1/installs` and `/v1/attribution`.
|
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.9.0";
|