@onborn/billing 0.1.0-beta.1
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/CHANGELOG.md +7 -0
- package/LICENSE +8 -0
- package/README.md +49 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/mock.d.ts +14 -0
- package/dist/adapters/mock.js +88 -0
- package/dist/adapters/nativeStores.d.ts +46 -0
- package/dist/adapters/nativeStores.js +166 -0
- package/dist/adapters/revenueCat.d.ts +90 -0
- package/dist/adapters/revenueCat.js +208 -0
- package/dist/client.d.ts +58 -0
- package/dist/client.js +148 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/runtime.d.ts +5 -0
- package/dist/runtime.js +38 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +1 -0
- package/dist/useOnbornEntitlements.d.ts +12 -0
- package/dist/useOnbornEntitlements.js +59 -0
- package/dist/useOnbornOffering.d.ts +29 -0
- package/dist/useOnbornOffering.js +194 -0
- package/dist/useOnbornPaywall.d.ts +31 -0
- package/dist/useOnbornPaywall.js +345 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +58 -0
- package/dist/validation.d.ts +20 -0
- package/dist/validation.js +60 -0
- package/package.json +61 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright (c) 2026 Onborn.
|
|
2
|
+
|
|
3
|
+
All rights reserved.
|
|
4
|
+
|
|
5
|
+
This package is proprietary software. It is published only for use with Onborn
|
|
6
|
+
products and services. No permission is granted to copy, modify, sublicense,
|
|
7
|
+
redistribute, or use this software outside an authorized Onborn integration
|
|
8
|
+
unless a separate written agreement says otherwise.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @onborn/billing
|
|
2
|
+
|
|
3
|
+
Headless Onborn billing for apps that own their onboarding and paywall UI.
|
|
4
|
+
|
|
5
|
+
It provides offering and paywall data, native-store adapters, purchase and
|
|
6
|
+
restore validation, and entitlement state without installing the Onborn UI
|
|
7
|
+
renderer.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
Onborn,
|
|
12
|
+
createNativeStoresBillingAdapter,
|
|
13
|
+
useOnbornEntitlements,
|
|
14
|
+
} from "@onborn/billing";
|
|
15
|
+
|
|
16
|
+
Onborn.init({
|
|
17
|
+
apiKey: "onborn_test_...",
|
|
18
|
+
userId: "customer_123",
|
|
19
|
+
appVersion: "1.0.0",
|
|
20
|
+
platform: "ios",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const billingAdapter = createNativeStoresBillingAdapter({
|
|
24
|
+
purchaseProduct: async ({ storeProductId }) => {
|
|
25
|
+
// Call expo-iap, react-native-iap, or your native purchase module.
|
|
26
|
+
return { productId: storeProductId };
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function PremiumGate() {
|
|
31
|
+
const { hasEntitlement, loading } = useOnbornEntitlements();
|
|
32
|
+
return { loading, premium: hasEntitlement("premium") };
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The package owns the Onborn API URL. Configure credentials and user context
|
|
37
|
+
once through `Onborn.init`; hooks and adapters do not accept an API key.
|
|
38
|
+
|
|
39
|
+
Installing `@onborn/billing` also installs its analytics and public-contract
|
|
40
|
+
dependencies. You do not need `@onborn/rn-sdk` unless you render Onborn-built
|
|
41
|
+
flows or paywalls.
|
|
42
|
+
|
|
43
|
+
`@onborn/rn-sdk` re-exports this package for apps that render Onborn flows.
|
|
44
|
+
|
|
45
|
+
## Documentation
|
|
46
|
+
|
|
47
|
+
- [Headless billing](../../docs/rn-sdk/billing.md)
|
|
48
|
+
- [Standalone analytics](../../docs/rn-sdk/analytics.md)
|
|
49
|
+
- [Full React Native SDK](../../docs/rn-sdk/rn-sdk.md)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OnbornBillingAdapter } from "../types";
|
|
2
|
+
export type MockBillingAdapterOptions = {
|
|
3
|
+
/** Prefix for synthetic transaction IDs sent to ONBORN validate. */
|
|
4
|
+
transactionIdPrefix?: string;
|
|
5
|
+
/** Optional delay to mimic store latency in dev. */
|
|
6
|
+
purchaseDelayMs?: number;
|
|
7
|
+
restoreDelayMs?: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Dev-only billing adapter: simulates successful store purchase/restore without
|
|
11
|
+
* RevenueCat or native IAP. Pair with ONBORN `test` environment for end-to-end
|
|
12
|
+
* paywall → validate → entitlement flow.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createMockBillingAdapter(options?: MockBillingAdapterOptions): OnbornBillingAdapter;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-only billing adapter: simulates successful store purchase/restore without
|
|
3
|
+
* RevenueCat or native IAP. Pair with ONBORN `test` environment for end-to-end
|
|
4
|
+
* paywall → validate → entitlement flow.
|
|
5
|
+
*/
|
|
6
|
+
export function createMockBillingAdapter(options = {}) {
|
|
7
|
+
const transactionIdPrefix = options.transactionIdPrefix ?? "mock";
|
|
8
|
+
return {
|
|
9
|
+
async purchasePackage(input) {
|
|
10
|
+
await delay(options.purchaseDelayMs);
|
|
11
|
+
const storeProductId = input.product?.storeProductId ?? input.package.productId;
|
|
12
|
+
if (!storeProductId) {
|
|
13
|
+
throw new Error(`Missing storeProductId for ONBORN package '${input.package.id}'.`);
|
|
14
|
+
}
|
|
15
|
+
const transactionId = `${transactionIdPrefix}-${Date.now()}-${input.package.id}`;
|
|
16
|
+
return {
|
|
17
|
+
success: true,
|
|
18
|
+
packageId: input.package.id,
|
|
19
|
+
productId: storeProductId,
|
|
20
|
+
transactionId,
|
|
21
|
+
receipt: JSON.stringify({
|
|
22
|
+
mock: true,
|
|
23
|
+
userId: input.userId,
|
|
24
|
+
offeringId: input.offering.id,
|
|
25
|
+
packageId: input.package.id,
|
|
26
|
+
storeProductId,
|
|
27
|
+
transactionId,
|
|
28
|
+
}),
|
|
29
|
+
raw: {
|
|
30
|
+
mock: true,
|
|
31
|
+
mode: "purchase",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
async restorePurchases(input) {
|
|
36
|
+
await delay(options.restoreDelayMs);
|
|
37
|
+
const activeProductIds = input.products
|
|
38
|
+
.map((product) => product.storeProductId)
|
|
39
|
+
.filter((id) => Boolean(id?.trim()));
|
|
40
|
+
const purchases = input.products
|
|
41
|
+
.filter((product) => (product.store === "app_store" ||
|
|
42
|
+
product.store === "google_play") &&
|
|
43
|
+
Boolean(product.storeProductId?.trim()))
|
|
44
|
+
.map((product) => {
|
|
45
|
+
const storeProductId = product.storeProductId ?? product.id;
|
|
46
|
+
return {
|
|
47
|
+
store: product.store,
|
|
48
|
+
storeProductId,
|
|
49
|
+
transactionId: `${transactionIdPrefix}-restore-${Date.now()}-${product.id}`,
|
|
50
|
+
raw: {
|
|
51
|
+
mock: true,
|
|
52
|
+
mode: "restore_purchase",
|
|
53
|
+
productId: product.id,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
return {
|
|
58
|
+
success: true,
|
|
59
|
+
activeProductIds,
|
|
60
|
+
entitlementIds: [],
|
|
61
|
+
purchases,
|
|
62
|
+
raw: {
|
|
63
|
+
mock: true,
|
|
64
|
+
mode: "restore",
|
|
65
|
+
userId: input.userId,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
async refetchCustomerEntitlements(input) {
|
|
70
|
+
return {
|
|
71
|
+
success: true,
|
|
72
|
+
raw: {
|
|
73
|
+
mock: true,
|
|
74
|
+
mode: "refetch",
|
|
75
|
+
userId: input.userId,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function delay(ms) {
|
|
82
|
+
if (!ms || ms <= 0) {
|
|
83
|
+
return Promise.resolve();
|
|
84
|
+
}
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
setTimeout(resolve, ms);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { NativeStoreRestoredPurchase } from "@onborn/sdk-contracts";
|
|
2
|
+
import type { OnbornBillingAdapter, OnbornLoadProductsInput, OnbornPurchaseInput, OnbornRestoreInput } from "../types";
|
|
3
|
+
type NativeStoresPurchase = {
|
|
4
|
+
productId?: string;
|
|
5
|
+
storeProductId?: string;
|
|
6
|
+
transactionId?: string;
|
|
7
|
+
purchaseToken?: string;
|
|
8
|
+
originalTransactionIdentifierIOS?: string;
|
|
9
|
+
raw?: unknown;
|
|
10
|
+
};
|
|
11
|
+
type NativeStoresProduct = {
|
|
12
|
+
productId?: string;
|
|
13
|
+
storeProductId?: string;
|
|
14
|
+
id?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
price?: string | number;
|
|
18
|
+
priceString?: string;
|
|
19
|
+
localizedPrice?: string;
|
|
20
|
+
localizedPriceString?: string;
|
|
21
|
+
displayPrice?: string;
|
|
22
|
+
formattedPrice?: string;
|
|
23
|
+
currency?: string;
|
|
24
|
+
currencyCode?: string;
|
|
25
|
+
period?: string;
|
|
26
|
+
subscriptionPeriod?: string;
|
|
27
|
+
raw?: unknown;
|
|
28
|
+
};
|
|
29
|
+
type NativeStoresRestoreOutput = NativeStoreRestoredPurchase[] | {
|
|
30
|
+
purchases?: NativeStoreRestoredPurchase[];
|
|
31
|
+
raw?: unknown;
|
|
32
|
+
} | unknown[] | void;
|
|
33
|
+
export type NativeStoresBillingAdapterOptions = {
|
|
34
|
+
loadProducts?: (input: OnbornLoadProductsInput & {
|
|
35
|
+
storeProductIds: string[];
|
|
36
|
+
}) => Promise<NativeStoresProduct[] | void>;
|
|
37
|
+
purchaseProduct: (input: OnbornPurchaseInput & {
|
|
38
|
+
storeProductId: string;
|
|
39
|
+
}) => Promise<NativeStoresPurchase | void>;
|
|
40
|
+
restorePurchases?: (input: OnbornRestoreInput) => Promise<NativeStoresRestoreOutput>;
|
|
41
|
+
refetchCustomerEntitlements?: (input: {
|
|
42
|
+
userId?: string;
|
|
43
|
+
}) => Promise<unknown>;
|
|
44
|
+
};
|
|
45
|
+
export declare function createNativeStoresBillingAdapter(options: NativeStoresBillingAdapterOptions): OnbornBillingAdapter;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
export function createNativeStoresBillingAdapter(options) {
|
|
2
|
+
return {
|
|
3
|
+
async loadProducts(input) {
|
|
4
|
+
if (!options.loadProducts) {
|
|
5
|
+
return input.products;
|
|
6
|
+
}
|
|
7
|
+
const storeProductIds = input.products
|
|
8
|
+
.map((product) => product.storeProductId)
|
|
9
|
+
.filter((id) => Boolean(id?.trim()));
|
|
10
|
+
if (storeProductIds.length === 0) {
|
|
11
|
+
return input.products;
|
|
12
|
+
}
|
|
13
|
+
const nativeProducts = (await options.loadProducts({
|
|
14
|
+
...input,
|
|
15
|
+
storeProductIds: Array.from(new Set(storeProductIds)),
|
|
16
|
+
})) ?? [];
|
|
17
|
+
if (nativeProducts.length === 0) {
|
|
18
|
+
return input.products;
|
|
19
|
+
}
|
|
20
|
+
return localizeProducts(input.products, nativeProducts);
|
|
21
|
+
},
|
|
22
|
+
async purchasePackage(input) {
|
|
23
|
+
const storeProductId = input.product?.storeProductId;
|
|
24
|
+
if (!storeProductId) {
|
|
25
|
+
throw new Error(`Missing storeProductId for ONBORN package '${input.package.id}'.`);
|
|
26
|
+
}
|
|
27
|
+
const purchase = await options.purchaseProduct({
|
|
28
|
+
...input,
|
|
29
|
+
storeProductId,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
success: true,
|
|
33
|
+
packageId: input.package.id,
|
|
34
|
+
productId: storeProductId,
|
|
35
|
+
transactionId: readTransactionId(purchase),
|
|
36
|
+
purchaseToken: purchase?.purchaseToken,
|
|
37
|
+
raw: purchase,
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
async restorePurchases(input) {
|
|
41
|
+
if (!options.restorePurchases) {
|
|
42
|
+
throw new Error("Missing native store restorePurchases implementation.");
|
|
43
|
+
}
|
|
44
|
+
const result = await options.restorePurchases(input);
|
|
45
|
+
const normalized = normalizeNativeRestoreOutput(result);
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
purchases: normalized.purchases,
|
|
49
|
+
raw: normalized.raw,
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
async refetchCustomerEntitlements(input) {
|
|
53
|
+
const result = await options.refetchCustomerEntitlements?.(input);
|
|
54
|
+
return {
|
|
55
|
+
success: true,
|
|
56
|
+
raw: result,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function localizeProducts(products, nativeProducts) {
|
|
62
|
+
return products.map((product) => {
|
|
63
|
+
const nativeProduct = findNativeProduct(product, nativeProducts);
|
|
64
|
+
if (!nativeProduct) {
|
|
65
|
+
return product;
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
...product,
|
|
69
|
+
title: readString(nativeProduct.title) ?? product.title,
|
|
70
|
+
description: readString(nativeProduct.description) ?? product.description,
|
|
71
|
+
price: readNativePrice(nativeProduct) ?? product.price,
|
|
72
|
+
currency: readString(nativeProduct.currencyCode) ??
|
|
73
|
+
readString(nativeProduct.currency) ??
|
|
74
|
+
product.currency,
|
|
75
|
+
period: readString(nativeProduct.subscriptionPeriod) ??
|
|
76
|
+
readString(nativeProduct.period) ??
|
|
77
|
+
product.period,
|
|
78
|
+
metadata: {
|
|
79
|
+
...product.metadata,
|
|
80
|
+
nativeStoreProduct: nativeProduct.raw ?? nativeProduct,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function findNativeProduct(product, nativeProducts) {
|
|
86
|
+
return nativeProducts.find((nativeProduct) => {
|
|
87
|
+
const identifiers = [
|
|
88
|
+
nativeProduct.storeProductId,
|
|
89
|
+
nativeProduct.productId,
|
|
90
|
+
nativeProduct.id,
|
|
91
|
+
].filter(Boolean);
|
|
92
|
+
return (identifiers.includes(product.storeProductId) ||
|
|
93
|
+
identifiers.includes(product.id));
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function readNativePrice(product) {
|
|
97
|
+
return (readString(product.priceString) ??
|
|
98
|
+
readString(product.localizedPriceString) ??
|
|
99
|
+
readString(product.localizedPrice) ??
|
|
100
|
+
readString(product.displayPrice) ??
|
|
101
|
+
readString(product.formattedPrice) ??
|
|
102
|
+
readPriceValue(product.price));
|
|
103
|
+
}
|
|
104
|
+
function normalizeNativeRestoreOutput(output) {
|
|
105
|
+
if (Array.isArray(output)) {
|
|
106
|
+
return {
|
|
107
|
+
purchases: output
|
|
108
|
+
.map(normalizeNativeRestoredPurchase)
|
|
109
|
+
.filter((item) => Boolean(item)),
|
|
110
|
+
raw: output,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (output && typeof output === "object") {
|
|
114
|
+
const result = output;
|
|
115
|
+
const purchases = Array.isArray(result.purchases)
|
|
116
|
+
? result.purchases
|
|
117
|
+
.map(normalizeNativeRestoredPurchase)
|
|
118
|
+
.filter((item) => Boolean(item))
|
|
119
|
+
: [];
|
|
120
|
+
return {
|
|
121
|
+
purchases,
|
|
122
|
+
raw: result.raw ?? output,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
purchases: [],
|
|
127
|
+
raw: output,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function normalizeNativeRestoredPurchase(value) {
|
|
131
|
+
if (!value || typeof value !== "object") {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const record = value;
|
|
135
|
+
const store = record.store;
|
|
136
|
+
const storeProductId = readString(record.storeProductId) ??
|
|
137
|
+
readString(record.productId) ??
|
|
138
|
+
readString(record.productIds);
|
|
139
|
+
if ((store !== "app_store" && store !== "google_play") || !storeProductId) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
store,
|
|
144
|
+
storeProductId,
|
|
145
|
+
transactionId: readString(record.transactionId) ??
|
|
146
|
+
readString(record.originalTransactionIdentifierIOS),
|
|
147
|
+
purchaseToken: readString(record.purchaseToken),
|
|
148
|
+
raw: record.raw ?? value,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function readTransactionId(purchase) {
|
|
152
|
+
return (purchase?.transactionId ??
|
|
153
|
+
purchase?.purchaseToken ??
|
|
154
|
+
purchase?.originalTransactionIdentifierIOS);
|
|
155
|
+
}
|
|
156
|
+
function readString(value) {
|
|
157
|
+
return typeof value === "string" && value.trim().length > 0
|
|
158
|
+
? value
|
|
159
|
+
: undefined;
|
|
160
|
+
}
|
|
161
|
+
function readPriceValue(value) {
|
|
162
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
163
|
+
return String(value);
|
|
164
|
+
}
|
|
165
|
+
return readString(value);
|
|
166
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { BillingRuntimeConfig } from "@onborn/sdk-contracts";
|
|
2
|
+
import type { OnbornBillingAdapter, OnbornPurchaseInput } from "../types";
|
|
3
|
+
type RevenueCatCustomerInfo = {
|
|
4
|
+
originalAppUserId?: string;
|
|
5
|
+
originalAppUserID?: string;
|
|
6
|
+
entitlements?: {
|
|
7
|
+
active?: Record<string, unknown>;
|
|
8
|
+
};
|
|
9
|
+
activeSubscriptions?: string[];
|
|
10
|
+
allPurchasedProductIdentifiers?: string[];
|
|
11
|
+
nonSubscriptionTransactions?: Array<{
|
|
12
|
+
productIdentifier?: string;
|
|
13
|
+
productId?: string;
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
16
|
+
type RevenueCatStoreProduct = {
|
|
17
|
+
identifier?: string;
|
|
18
|
+
productIdentifier?: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
price?: number | string;
|
|
22
|
+
priceString?: string;
|
|
23
|
+
localizedPriceString?: string;
|
|
24
|
+
currencyCode?: string;
|
|
25
|
+
currency?: string;
|
|
26
|
+
subscriptionPeriod?: string;
|
|
27
|
+
period?: string;
|
|
28
|
+
trialPeriod?: string;
|
|
29
|
+
trialDuration?: string;
|
|
30
|
+
trial_duration?: string;
|
|
31
|
+
freeTrialPeriod?: string;
|
|
32
|
+
introPrice?: {
|
|
33
|
+
period?: string;
|
|
34
|
+
subscriptionPeriod?: string;
|
|
35
|
+
} | null;
|
|
36
|
+
introductoryPrice?: {
|
|
37
|
+
period?: string;
|
|
38
|
+
subscriptionPeriod?: string;
|
|
39
|
+
} | null;
|
|
40
|
+
};
|
|
41
|
+
type RevenueCatPackage = {
|
|
42
|
+
identifier?: string;
|
|
43
|
+
packageType?: string;
|
|
44
|
+
product?: RevenueCatStoreProduct;
|
|
45
|
+
};
|
|
46
|
+
type RevenueCatOffering = {
|
|
47
|
+
identifier?: string;
|
|
48
|
+
availablePackages?: RevenueCatPackage[];
|
|
49
|
+
};
|
|
50
|
+
type RevenueCatOfferings = {
|
|
51
|
+
current?: RevenueCatOffering | null;
|
|
52
|
+
all?: Record<string, RevenueCatOffering | undefined>;
|
|
53
|
+
};
|
|
54
|
+
export type RevenueCatPurchasesModule = {
|
|
55
|
+
configure?: (options: {
|
|
56
|
+
apiKey: string;
|
|
57
|
+
appUserID?: string;
|
|
58
|
+
}) => void | Promise<void>;
|
|
59
|
+
getOfferings: () => Promise<RevenueCatOfferings>;
|
|
60
|
+
purchasePackage: (pkg: RevenueCatPackage) => Promise<{
|
|
61
|
+
customerInfo?: RevenueCatCustomerInfo;
|
|
62
|
+
productIdentifier?: string;
|
|
63
|
+
}>;
|
|
64
|
+
restorePurchases: () => Promise<RevenueCatCustomerInfo>;
|
|
65
|
+
getCustomerInfo?: () => Promise<RevenueCatCustomerInfo>;
|
|
66
|
+
logIn?: (appUserId: string) => Promise<{
|
|
67
|
+
customerInfo?: RevenueCatCustomerInfo;
|
|
68
|
+
created?: boolean;
|
|
69
|
+
}>;
|
|
70
|
+
};
|
|
71
|
+
export type RevenueCatBillingAdapterOptions = {
|
|
72
|
+
purchases: RevenueCatPurchasesModule;
|
|
73
|
+
/**
|
|
74
|
+
* Calls RevenueCat `logIn(userId)` before purchase/restore/refetch when the
|
|
75
|
+
* method exists. Keep this on unless the host app configures RevenueCat with
|
|
76
|
+
* the same appUserID before creating the adapter.
|
|
77
|
+
*/
|
|
78
|
+
identifyUser?: boolean;
|
|
79
|
+
resolvePackage?: (input: OnbornPurchaseInput, offerings: RevenueCatOfferings) => RevenueCatPackage | Promise<RevenueCatPackage>;
|
|
80
|
+
};
|
|
81
|
+
export declare function createRevenueCatBillingAdapter(options: RevenueCatBillingAdapterOptions): OnbornBillingAdapter;
|
|
82
|
+
export type ConfigureRevenueCatPurchasesOptions = {
|
|
83
|
+
purchases: RevenueCatPurchasesModule;
|
|
84
|
+
billing?: BillingRuntimeConfig;
|
|
85
|
+
platform?: "ios" | "android";
|
|
86
|
+
userId?: string;
|
|
87
|
+
};
|
|
88
|
+
export declare function configureRevenueCatPurchases(options: ConfigureRevenueCatPurchasesOptions): Promise<void>;
|
|
89
|
+
export declare function resolveRevenueCatSdkKey(options: Pick<ConfigureRevenueCatPurchasesOptions, "billing" | "platform">): string | undefined;
|
|
90
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
export function createRevenueCatBillingAdapter(options) {
|
|
3
|
+
return {
|
|
4
|
+
async loadProducts(input) {
|
|
5
|
+
await identifyRevenueCatUser(options, input.userId);
|
|
6
|
+
const offerings = await options.purchases.getOfferings();
|
|
7
|
+
return localizeProductsFromRevenueCat(input, offerings);
|
|
8
|
+
},
|
|
9
|
+
async purchasePackage(input) {
|
|
10
|
+
await identifyRevenueCatUser(options, input.userId);
|
|
11
|
+
const offerings = await options.purchases.getOfferings();
|
|
12
|
+
const revenueCatPackage = options.resolvePackage
|
|
13
|
+
? await options.resolvePackage(input, offerings)
|
|
14
|
+
: resolveRevenueCatPackage(input, offerings);
|
|
15
|
+
const result = await options.purchases.purchasePackage(revenueCatPackage);
|
|
16
|
+
return {
|
|
17
|
+
success: true,
|
|
18
|
+
packageId: input.package.id,
|
|
19
|
+
productId: result.productIdentifier ??
|
|
20
|
+
input.product?.storeProductId ??
|
|
21
|
+
input.package.productId,
|
|
22
|
+
entitlementIds: readActiveEntitlementIds(result.customerInfo),
|
|
23
|
+
activeProductIds: readActiveProductIds(result.customerInfo),
|
|
24
|
+
raw: result,
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
async restorePurchases(input) {
|
|
28
|
+
await identifyRevenueCatUser(options, input.userId);
|
|
29
|
+
const customerInfo = await options.purchases.restorePurchases();
|
|
30
|
+
return {
|
|
31
|
+
success: true,
|
|
32
|
+
entitlementIds: readActiveEntitlementIds(customerInfo),
|
|
33
|
+
activeProductIds: readActiveProductIds(customerInfo),
|
|
34
|
+
raw: {
|
|
35
|
+
customerInfo,
|
|
36
|
+
productIds: input.products.map((product) => product.storeProductId),
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
async refetchCustomerEntitlements(input) {
|
|
41
|
+
await identifyRevenueCatUser(options, input.userId);
|
|
42
|
+
if (!options.purchases.getCustomerInfo) {
|
|
43
|
+
throw new Error("Missing RevenueCat getCustomerInfo implementation.");
|
|
44
|
+
}
|
|
45
|
+
const customerInfo = await options.purchases.getCustomerInfo();
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
entitlementIds: readActiveEntitlementIds(customerInfo),
|
|
49
|
+
activeProductIds: readActiveProductIds(customerInfo),
|
|
50
|
+
raw: { customerInfo },
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export async function configureRevenueCatPurchases(options) {
|
|
56
|
+
if (!options.purchases.configure) {
|
|
57
|
+
throw new Error("Missing RevenueCat configure implementation.");
|
|
58
|
+
}
|
|
59
|
+
const apiKey = resolveRevenueCatSdkKey(options);
|
|
60
|
+
if (!apiKey) {
|
|
61
|
+
throw new Error("Missing RevenueCat public SDK key for current platform.");
|
|
62
|
+
}
|
|
63
|
+
await options.purchases.configure({
|
|
64
|
+
apiKey,
|
|
65
|
+
...(options.userId
|
|
66
|
+
? {
|
|
67
|
+
appUserID: options.userId,
|
|
68
|
+
}
|
|
69
|
+
: {}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export function resolveRevenueCatSdkKey(options) {
|
|
73
|
+
if (options.billing?.provider !== "revenuecat") {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const platform = options.platform ?? resolveRuntimePlatform();
|
|
77
|
+
return platform === "android"
|
|
78
|
+
? options.billing.revenueCat?.androidSdkKey
|
|
79
|
+
: options.billing.revenueCat?.iosSdkKey;
|
|
80
|
+
}
|
|
81
|
+
async function identifyRevenueCatUser(options, userId) {
|
|
82
|
+
if (options.identifyUser === false || !userId || !options.purchases.logIn) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await options.purchases.logIn(userId);
|
|
86
|
+
}
|
|
87
|
+
function resolveRuntimePlatform() {
|
|
88
|
+
return Platform.OS === "android" ? "android" : "ios";
|
|
89
|
+
}
|
|
90
|
+
function resolveRevenueCatPackage(input, offerings) {
|
|
91
|
+
const match = findRevenueCatPackage(input, offerings);
|
|
92
|
+
if (!match) {
|
|
93
|
+
throw new Error(`RevenueCat package not found for ONBORN package '${input.package.id}'.`);
|
|
94
|
+
}
|
|
95
|
+
return match;
|
|
96
|
+
}
|
|
97
|
+
function findRevenueCatPackage(input, offerings) {
|
|
98
|
+
const candidates = listRevenueCatPackages(input, offerings);
|
|
99
|
+
const productId = input.product?.storeProductId;
|
|
100
|
+
return candidates.find((candidate) => {
|
|
101
|
+
const identifiers = [
|
|
102
|
+
candidate.identifier,
|
|
103
|
+
candidate.packageType,
|
|
104
|
+
candidate.product?.identifier,
|
|
105
|
+
candidate.product?.productIdentifier,
|
|
106
|
+
].filter(Boolean);
|
|
107
|
+
return (identifiers.includes(input.package.id) ||
|
|
108
|
+
identifiers.includes(input.package.productId) ||
|
|
109
|
+
(productId ? identifiers.includes(productId) : false));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function listRevenueCatPackages(input, offerings) {
|
|
113
|
+
const offeringKeys = [
|
|
114
|
+
input.offering.providerOfferingId,
|
|
115
|
+
input.offering.key,
|
|
116
|
+
input.offering.id,
|
|
117
|
+
].filter((key) => Boolean(key));
|
|
118
|
+
const preferredOfferings = offeringKeys
|
|
119
|
+
.map((key) => offerings.all?.[key])
|
|
120
|
+
.filter((offering) => Boolean(offering));
|
|
121
|
+
const allOfferings = Object.values(offerings.all ?? {}).filter((offering) => Boolean(offering));
|
|
122
|
+
return [...preferredOfferings, offerings.current, ...allOfferings]
|
|
123
|
+
.filter((offering) => Boolean(offering))
|
|
124
|
+
.flatMap((offering) => offering.availablePackages ?? []);
|
|
125
|
+
}
|
|
126
|
+
function localizeProductsFromRevenueCat(input, offerings) {
|
|
127
|
+
if (!input.offering) {
|
|
128
|
+
return input.products;
|
|
129
|
+
}
|
|
130
|
+
const offering = input.offering;
|
|
131
|
+
return input.products.map((product) => {
|
|
132
|
+
const billingPackage = offering.packages.find((item) => packageReferencesProduct(item, product));
|
|
133
|
+
if (!billingPackage) {
|
|
134
|
+
return product;
|
|
135
|
+
}
|
|
136
|
+
const revenueCatPackage = findRevenueCatPackage({
|
|
137
|
+
paywall: input.paywall,
|
|
138
|
+
offering,
|
|
139
|
+
package: billingPackage,
|
|
140
|
+
product,
|
|
141
|
+
userId: input.userId,
|
|
142
|
+
}, offerings);
|
|
143
|
+
const revenueCatProduct = revenueCatPackage?.product;
|
|
144
|
+
if (!revenueCatProduct) {
|
|
145
|
+
return product;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
...product,
|
|
149
|
+
title: revenueCatProduct.title ?? product.title,
|
|
150
|
+
description: revenueCatProduct.description ?? product.description,
|
|
151
|
+
price: readRevenueCatPrice(revenueCatProduct) ?? product.price,
|
|
152
|
+
currency: revenueCatProduct.currencyCode ??
|
|
153
|
+
revenueCatProduct.currency ??
|
|
154
|
+
product.currency,
|
|
155
|
+
period: revenueCatProduct.subscriptionPeriod ??
|
|
156
|
+
revenueCatProduct.period ??
|
|
157
|
+
product.period,
|
|
158
|
+
trialPeriod: readRevenueCatTrialPeriod(revenueCatProduct) ?? product.trialPeriod,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function packageReferencesProduct(billingPackage, product) {
|
|
163
|
+
const referencedIds = [
|
|
164
|
+
billingPackage.productId,
|
|
165
|
+
billingPackage.productIdsByStore?.app_store,
|
|
166
|
+
billingPackage.productIdsByStore?.google_play,
|
|
167
|
+
billingPackage.productIdsByStore?.revenuecat_custom,
|
|
168
|
+
].filter((value) => Boolean(value));
|
|
169
|
+
return (referencedIds.includes(product.id) ||
|
|
170
|
+
referencedIds.includes(product.storeProductId));
|
|
171
|
+
}
|
|
172
|
+
function readRevenueCatPrice(product) {
|
|
173
|
+
if (product.priceString) {
|
|
174
|
+
return product.priceString;
|
|
175
|
+
}
|
|
176
|
+
if (product.localizedPriceString) {
|
|
177
|
+
return product.localizedPriceString;
|
|
178
|
+
}
|
|
179
|
+
if (typeof product.price === "string") {
|
|
180
|
+
return product.price;
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
function readRevenueCatTrialPeriod(product) {
|
|
185
|
+
return (product.trialPeriod ??
|
|
186
|
+
product.trialDuration ??
|
|
187
|
+
product.trial_duration ??
|
|
188
|
+
product.freeTrialPeriod ??
|
|
189
|
+
product.introPrice?.period ??
|
|
190
|
+
product.introPrice?.subscriptionPeriod ??
|
|
191
|
+
product.introductoryPrice?.period ??
|
|
192
|
+
product.introductoryPrice?.subscriptionPeriod);
|
|
193
|
+
}
|
|
194
|
+
function readActiveEntitlementIds(customerInfo) {
|
|
195
|
+
return Object.keys(customerInfo?.entitlements?.active ?? {});
|
|
196
|
+
}
|
|
197
|
+
function readActiveProductIds(customerInfo) {
|
|
198
|
+
return uniqueCompact([
|
|
199
|
+
...(customerInfo?.activeSubscriptions ?? []),
|
|
200
|
+
...(customerInfo?.allPurchasedProductIdentifiers ?? []),
|
|
201
|
+
...(customerInfo?.nonSubscriptionTransactions ?? []).flatMap((transaction) => [transaction.productIdentifier, transaction.productId]),
|
|
202
|
+
]);
|
|
203
|
+
}
|
|
204
|
+
function uniqueCompact(values) {
|
|
205
|
+
return [
|
|
206
|
+
...new Set(values.filter((value) => Boolean(value))),
|
|
207
|
+
];
|
|
208
|
+
}
|