@sudobility/consumables_client 0.0.2
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/CLAUDE.md +69 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/revenuecat-rn.d.ts +7 -0
- package/dist/adapters/revenuecat-rn.d.ts.map +1 -0
- package/dist/adapters/revenuecat-rn.js +129 -0
- package/dist/adapters/revenuecat-rn.js.map +1 -0
- package/dist/adapters/revenuecat-web.d.ts +7 -0
- package/dist/adapters/revenuecat-web.d.ts.map +1 -0
- package/dist/adapters/revenuecat-web.js +143 -0
- package/dist/adapters/revenuecat-web.js.map +1 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/service.d.ts +31 -0
- package/dist/core/service.d.ts.map +1 -0
- package/dist/core/service.js +92 -0
- package/dist/core/service.js.map +1 -0
- package/dist/core/singleton.d.ts +18 -0
- package/dist/core/singleton.d.ts.map +1 -0
- package/dist/core/singleton.js +74 -0
- package/dist/core/singleton.js.map +1 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/useBalance.d.ts +9 -0
- package/dist/hooks/useBalance.d.ts.map +1 -0
- package/dist/hooks/useBalance.js +53 -0
- package/dist/hooks/useBalance.js.map +1 -0
- package/dist/hooks/useConsumableProducts.d.ts +9 -0
- package/dist/hooks/useConsumableProducts.d.ts.map +1 -0
- package/dist/hooks/useConsumableProducts.js +32 -0
- package/dist/hooks/useConsumableProducts.js.map +1 -0
- package/dist/hooks/usePurchaseCredits.d.ts +7 -0
- package/dist/hooks/usePurchaseCredits.d.ts.map +1 -0
- package/dist/hooks/usePurchaseCredits.js +29 -0
- package/dist/hooks/usePurchaseCredits.js.map +1 -0
- package/dist/hooks/usePurchaseHistory.d.ts +10 -0
- package/dist/hooks/usePurchaseHistory.d.ts.map +1 -0
- package/dist/hooks/usePurchaseHistory.js +48 -0
- package/dist/hooks/usePurchaseHistory.js.map +1 -0
- package/dist/hooks/useUsageHistory.d.ts +10 -0
- package/dist/hooks/useUsageHistory.d.ts.map +1 -0
- package/dist/hooks/useUsageHistory.js +48 -0
- package/dist/hooks/useUsageHistory.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/network/ConsumablesApiClient.d.ts +28 -0
- package/dist/network/ConsumablesApiClient.d.ts.map +1 -0
- package/dist/network/ConsumablesApiClient.js +55 -0
- package/dist/network/ConsumablesApiClient.js.map +1 -0
- package/dist/types/adapter.d.ts +25 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +2 -0
- package/dist/types/adapter.js.map +1 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +65 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Consumables Client
|
|
2
|
+
|
|
3
|
+
Cross-platform consumable credits client with RevenueCat adapter pattern.
|
|
4
|
+
|
|
5
|
+
**npm**: `@sudobility/consumables_client` (public)
|
|
6
|
+
|
|
7
|
+
## Tech Stack
|
|
8
|
+
|
|
9
|
+
- **Language**: TypeScript (strict mode)
|
|
10
|
+
- **Runtime**: Bun
|
|
11
|
+
- **Package Manager**: Bun
|
|
12
|
+
- **Build**: TypeScript compiler (ESM)
|
|
13
|
+
- **Test**: vitest
|
|
14
|
+
|
|
15
|
+
## Project Structure
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
src/
|
|
19
|
+
├── index.ts # Main exports
|
|
20
|
+
├── types/
|
|
21
|
+
│ ├── index.ts # CreditPackage, CreditBalance, etc.
|
|
22
|
+
│ └── adapter.ts # ConsumablesAdapter interface
|
|
23
|
+
├── core/
|
|
24
|
+
│ ├── index.ts
|
|
25
|
+
│ ├── service.ts # ConsumablesService class
|
|
26
|
+
│ └── singleton.ts # Global singleton + event listeners
|
|
27
|
+
├── network/
|
|
28
|
+
│ └── ConsumablesApiClient.ts # HTTP wrapper for API calls
|
|
29
|
+
├── adapters/
|
|
30
|
+
│ ├── index.ts
|
|
31
|
+
│ ├── revenuecat-web.ts # Web adapter
|
|
32
|
+
│ └── revenuecat-rn.ts # React Native adapter
|
|
33
|
+
└── hooks/
|
|
34
|
+
├── index.ts
|
|
35
|
+
├── useBalance.ts
|
|
36
|
+
├── useConsumableProducts.ts
|
|
37
|
+
├── usePurchaseCredits.ts
|
|
38
|
+
├── usePurchaseHistory.ts
|
|
39
|
+
└── useUsageHistory.ts
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bun run build # Build ESM
|
|
46
|
+
bun run clean # Remove dist/
|
|
47
|
+
bun run dev # Watch mode
|
|
48
|
+
bun test # Run tests
|
|
49
|
+
bun run typecheck # TypeScript check
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Key Concepts
|
|
53
|
+
|
|
54
|
+
### Adapter Pattern
|
|
55
|
+
- Web adapter wraps @revenuecat/purchases-js
|
|
56
|
+
- RN adapter wraps react-native-purchases
|
|
57
|
+
- Both implement ConsumablesAdapter interface
|
|
58
|
+
|
|
59
|
+
### Singleton + Event Listeners
|
|
60
|
+
- No React Context needed
|
|
61
|
+
- initializeConsumables() at app startup
|
|
62
|
+
- setConsumablesUserId() on auth change
|
|
63
|
+
- onConsumablesBalanceChange() for reactivity
|
|
64
|
+
|
|
65
|
+
### Hooks
|
|
66
|
+
- useBalance() — current credit balance
|
|
67
|
+
- useConsumableProducts(offeringId) — available packages
|
|
68
|
+
- usePurchaseCredits() — purchase flow
|
|
69
|
+
- usePurchaseHistory() / useUsageHistory() — audit trails
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { configureConsumablesWebAdapter, createConsumablesWebAdapter, setConsumablesWebUser, clearConsumablesWebUser, hasConsumablesWebUser, } from "./revenuecat-web";
|
|
2
|
+
export { configureConsumablesRNAdapter, createConsumablesRNAdapter, setConsumablesRNUser, clearConsumablesRNUser, hasConsumablesRNUser, } from "./revenuecat-rn";
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,8BAA8B,EAC9B,2BAA2B,EAC3B,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,6BAA6B,EAC7B,0BAA0B,EAC1B,oBAAoB,EACpB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { configureConsumablesWebAdapter, createConsumablesWebAdapter, setConsumablesWebUser, clearConsumablesWebUser, hasConsumablesWebUser, } from "./revenuecat-web";
|
|
2
|
+
export { configureConsumablesRNAdapter, createConsumablesRNAdapter, setConsumablesRNUser, clearConsumablesRNUser, hasConsumablesRNUser, } from "./revenuecat-rn";
|
|
3
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,8BAA8B,EAC9B,2BAA2B,EAC3B,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,6BAA6B,EAC7B,0BAA0B,EAC1B,oBAAoB,EACpB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC","sourcesContent":["// Web adapter\nexport {\n configureConsumablesWebAdapter,\n createConsumablesWebAdapter,\n setConsumablesWebUser,\n clearConsumablesWebUser,\n hasConsumablesWebUser,\n} from \"./revenuecat-web\";\n\n// React Native adapter\nexport {\n configureConsumablesRNAdapter,\n createConsumablesRNAdapter,\n setConsumablesRNUser,\n clearConsumablesRNUser,\n hasConsumablesRNUser,\n} from \"./revenuecat-rn\";\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ConsumablesAdapter } from "../types/adapter";
|
|
2
|
+
export declare function configureConsumablesRNAdapter(revenueCatApiKey: string): void;
|
|
3
|
+
export declare function setConsumablesRNUser(userId: string, email?: string): Promise<void>;
|
|
4
|
+
export declare function clearConsumablesRNUser(): Promise<void>;
|
|
5
|
+
export declare function hasConsumablesRNUser(): boolean;
|
|
6
|
+
export declare function createConsumablesRNAdapter(): ConsumablesAdapter;
|
|
7
|
+
//# sourceMappingURL=revenuecat-rn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revenuecat-rn.d.ts","sourceRoot":"","sources":["../../src/adapters/revenuecat-rn.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAGV,kBAAkB,EACnB,MAAM,kBAAkB,CAAC;AAQ1B,wBAAgB,6BAA6B,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAE5E;AAsBD,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAoBf;AAGD,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC,CAQ5D;AAGD,wBAAgB,oBAAoB,IAAI,OAAO,CAE9C;AAGD,wBAAgB,0BAA0B,IAAI,kBAAkB,CAoG/D"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
let apiKey = null;
|
|
2
|
+
let currentUserId = null;
|
|
3
|
+
let isConfigured = false;
|
|
4
|
+
export function configureConsumablesRNAdapter(revenueCatApiKey) {
|
|
5
|
+
apiKey = revenueCatApiKey;
|
|
6
|
+
}
|
|
7
|
+
async function getRNPurchases() {
|
|
8
|
+
try {
|
|
9
|
+
return require("react-native-purchases");
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new Error("react-native-purchases is not installed");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function ensureConfigured() {
|
|
16
|
+
const Purchases = await getRNPurchases();
|
|
17
|
+
if (!isConfigured && apiKey) {
|
|
18
|
+
Purchases.configure({ apiKey });
|
|
19
|
+
isConfigured = true;
|
|
20
|
+
}
|
|
21
|
+
return Purchases;
|
|
22
|
+
}
|
|
23
|
+
export async function setConsumablesRNUser(userId, email) {
|
|
24
|
+
if (!apiKey)
|
|
25
|
+
return;
|
|
26
|
+
if (currentUserId === userId)
|
|
27
|
+
return;
|
|
28
|
+
const Purchases = await ensureConfigured();
|
|
29
|
+
if (currentUserId) {
|
|
30
|
+
await Purchases.logOut();
|
|
31
|
+
}
|
|
32
|
+
await Purchases.logIn(userId);
|
|
33
|
+
currentUserId = userId;
|
|
34
|
+
if (email) {
|
|
35
|
+
try {
|
|
36
|
+
await Purchases.setAttributes({ email });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function clearConsumablesRNUser() {
|
|
43
|
+
try {
|
|
44
|
+
const Purchases = await getRNPurchases();
|
|
45
|
+
await Purchases.logOut();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
}
|
|
49
|
+
currentUserId = null;
|
|
50
|
+
}
|
|
51
|
+
export function hasConsumablesRNUser() {
|
|
52
|
+
return currentUserId !== null;
|
|
53
|
+
}
|
|
54
|
+
export function createConsumablesRNAdapter() {
|
|
55
|
+
return {
|
|
56
|
+
async setUserId(userId, email) {
|
|
57
|
+
if (userId) {
|
|
58
|
+
await setConsumablesRNUser(userId, email);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
await clearConsumablesRNUser();
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
async getOfferings() {
|
|
65
|
+
try {
|
|
66
|
+
const Purchases = await ensureConfigured();
|
|
67
|
+
const offerings = await Purchases.getOfferings();
|
|
68
|
+
const all = {};
|
|
69
|
+
for (const offering of Object.values(offerings.all)) {
|
|
70
|
+
all[offering.identifier] = {
|
|
71
|
+
identifier: offering.identifier,
|
|
72
|
+
metadata: offering.metadata || null,
|
|
73
|
+
packages: offering.availablePackages.map((pkg) => {
|
|
74
|
+
const metadata = pkg.product?.metadata || {};
|
|
75
|
+
const credits = typeof metadata.credits === "number" ? metadata.credits : 0;
|
|
76
|
+
return {
|
|
77
|
+
packageId: pkg.identifier,
|
|
78
|
+
productId: pkg.product?.identifier || "",
|
|
79
|
+
title: pkg.product?.title || "",
|
|
80
|
+
description: pkg.product?.description || null,
|
|
81
|
+
credits,
|
|
82
|
+
price: pkg.product?.price || 0,
|
|
83
|
+
priceString: pkg.product?.priceString || "$0",
|
|
84
|
+
currencyCode: pkg.product?.currencyCode || "USD",
|
|
85
|
+
};
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return { all };
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error("[consumables-rn] Failed to get offerings:", error);
|
|
93
|
+
return { all: {} };
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
async purchase(params) {
|
|
97
|
+
const Purchases = await ensureConfigured();
|
|
98
|
+
const offerings = await Purchases.getOfferings();
|
|
99
|
+
let packageToPurchase;
|
|
100
|
+
for (const offering of Object.values(offerings.all)) {
|
|
101
|
+
packageToPurchase = offering.availablePackages.find((pkg) => pkg.identifier === params.packageId);
|
|
102
|
+
if (packageToPurchase)
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
if (!packageToPurchase) {
|
|
106
|
+
throw new Error(`Package not found: ${params.packageId}`);
|
|
107
|
+
}
|
|
108
|
+
const result = await Purchases.purchasePackage(packageToPurchase);
|
|
109
|
+
const metadata = packageToPurchase.product?.metadata || {};
|
|
110
|
+
const credits = typeof metadata.credits === "number" ? metadata.credits : 0;
|
|
111
|
+
const nonSubTransactions = result.customerInfo?.nonSubscriptionTransactions || [];
|
|
112
|
+
const latestTransaction = nonSubTransactions[nonSubTransactions.length - 1];
|
|
113
|
+
const transactionId = latestTransaction?.transactionIdentifier || `rn_${Date.now()}`;
|
|
114
|
+
const platform = typeof navigator !== "undefined" &&
|
|
115
|
+
navigator.product === "ReactNative"
|
|
116
|
+
? "apple"
|
|
117
|
+
: "apple";
|
|
118
|
+
return {
|
|
119
|
+
transactionId,
|
|
120
|
+
productId: packageToPurchase.product?.identifier || "",
|
|
121
|
+
credits,
|
|
122
|
+
priceCents: Math.round((packageToPurchase.product?.price || 0) * 100),
|
|
123
|
+
currency: packageToPurchase.product?.currencyCode || "USD",
|
|
124
|
+
source: platform,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=revenuecat-rn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revenuecat-rn.js","sourceRoot":"","sources":["../../src/adapters/revenuecat-rn.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * RevenueCat React Native Adapter for Consumables\n *\n * Wraps react-native-purchases for consumable credit purchases.\n * Lazy loads the SDK with try/catch for environments where it's not installed.\n */\n\nimport type {\n ConsumablePurchaseParams,\n ConsumablePurchaseResult,\n ConsumablesAdapter,\n} from \"../types/adapter\";\nimport type { CreditPackage } from \"../types\";\n\nlet apiKey: string | null = null;\nlet currentUserId: string | null = null;\nlet isConfigured = false;\n\n/** Configure the RN adapter with RevenueCat API key. */\nexport function configureConsumablesRNAdapter(revenueCatApiKey: string): void {\n apiKey = revenueCatApiKey;\n}\n\nasync function getRNPurchases() {\n try {\n return require(\"react-native-purchases\");\n } catch {\n throw new Error(\"react-native-purchases is not installed\");\n }\n}\n\nasync function ensureConfigured(): Promise<any> {\n const Purchases = await getRNPurchases();\n\n if (!isConfigured && apiKey) {\n Purchases.configure({ apiKey });\n isConfigured = true;\n }\n\n return Purchases;\n}\n\n/** Set the current user for RevenueCat RN. */\nexport async function setConsumablesRNUser(\n userId: string,\n email?: string,\n): Promise<void> {\n if (!apiKey) return;\n if (currentUserId === userId) return;\n\n const Purchases = await ensureConfigured();\n\n if (currentUserId) {\n await Purchases.logOut();\n }\n\n await Purchases.logIn(userId);\n currentUserId = userId;\n\n if (email) {\n try {\n await Purchases.setAttributes({ email });\n } catch {\n // Ignore attribute errors\n }\n }\n}\n\n/** Clear the current user (on logout). */\nexport async function clearConsumablesRNUser(): Promise<void> {\n try {\n const Purchases = await getRNPurchases();\n await Purchases.logOut();\n } catch {\n // SDK not available\n }\n currentUserId = null;\n}\n\n/** Check if a user is configured. */\nexport function hasConsumablesRNUser(): boolean {\n return currentUserId !== null;\n}\n\n/** Create the consumables adapter for React Native. */\nexport function createConsumablesRNAdapter(): ConsumablesAdapter {\n return {\n async setUserId(userId: string | undefined, email?: string): Promise<void> {\n if (userId) {\n await setConsumablesRNUser(userId, email);\n } else {\n await clearConsumablesRNUser();\n }\n },\n\n async getOfferings() {\n try {\n const Purchases = await ensureConfigured();\n const offerings = await Purchases.getOfferings();\n\n const all: Record<\n string,\n {\n identifier: string;\n metadata: Record<string, unknown> | null;\n packages: CreditPackage[];\n }\n > = {};\n\n for (const offering of Object.values(offerings.all) as any[]) {\n all[offering.identifier] = {\n identifier: offering.identifier,\n metadata: offering.metadata || null,\n packages: offering.availablePackages.map((pkg: any) => {\n const metadata = pkg.product?.metadata || {};\n const credits =\n typeof metadata.credits === \"number\" ? metadata.credits : 0;\n return {\n packageId: pkg.identifier,\n productId: pkg.product?.identifier || \"\",\n title: pkg.product?.title || \"\",\n description: pkg.product?.description || null,\n credits,\n price: pkg.product?.price || 0,\n priceString: pkg.product?.priceString || \"$0\",\n currencyCode: pkg.product?.currencyCode || \"USD\",\n } satisfies CreditPackage;\n }),\n };\n }\n\n return { all };\n } catch (error) {\n console.error(\"[consumables-rn] Failed to get offerings:\", error);\n return { all: {} };\n }\n },\n\n async purchase(\n params: ConsumablePurchaseParams,\n ): Promise<ConsumablePurchaseResult> {\n const Purchases = await ensureConfigured();\n const offerings = await Purchases.getOfferings();\n\n let packageToPurchase: any;\n for (const offering of Object.values(offerings.all) as any[]) {\n packageToPurchase = offering.availablePackages.find(\n (pkg: any) => pkg.identifier === params.packageId,\n );\n if (packageToPurchase) break;\n }\n\n if (!packageToPurchase) {\n throw new Error(`Package not found: ${params.packageId}`);\n }\n\n const result = await Purchases.purchasePackage(packageToPurchase);\n const metadata = packageToPurchase.product?.metadata || {};\n const credits =\n typeof metadata.credits === \"number\" ? metadata.credits : 0;\n\n const nonSubTransactions =\n result.customerInfo?.nonSubscriptionTransactions || [];\n const latestTransaction =\n nonSubTransactions[nonSubTransactions.length - 1];\n const transactionId =\n latestTransaction?.transactionIdentifier || `rn_${Date.now()}`;\n\n // Determine source based on platform\n const platform =\n typeof navigator !== \"undefined\" &&\n (navigator as any).product === \"ReactNative\"\n ? \"apple\" // Default to apple for RN, could be refined with Platform.OS\n : \"apple\";\n\n return {\n transactionId,\n productId: packageToPurchase.product?.identifier || \"\",\n credits,\n priceCents: Math.round((packageToPurchase.product?.price || 0) * 100),\n currency: packageToPurchase.product?.currencyCode || \"USD\",\n source: platform as \"apple\" | \"google\",\n };\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ConsumablesAdapter } from "../types/adapter";
|
|
2
|
+
export declare function configureConsumablesWebAdapter(revenueCatApiKey: string): void;
|
|
3
|
+
export declare function setConsumablesWebUser(userId: string, email?: string): Promise<void>;
|
|
4
|
+
export declare function clearConsumablesWebUser(): void;
|
|
5
|
+
export declare function hasConsumablesWebUser(): boolean;
|
|
6
|
+
export declare function createConsumablesWebAdapter(): ConsumablesAdapter;
|
|
7
|
+
//# sourceMappingURL=revenuecat-web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revenuecat-web.d.ts","sourceRoot":"","sources":["../../src/adapters/revenuecat-web.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAGV,kBAAkB,EACnB,MAAM,kBAAkB,CAAC;AAc1B,wBAAgB,8BAA8B,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAE7E;AAmBD,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAwBf;AAGD,wBAAgB,uBAAuB,IAAI,IAAI,CAM9C;AAGD,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AAsBD,wBAAgB,2BAA2B,IAAI,kBAAkB,CAsFhE"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
let purchasesInstance = null;
|
|
2
|
+
let currentUserId = null;
|
|
3
|
+
let apiKey = null;
|
|
4
|
+
let pendingUserSetup = null;
|
|
5
|
+
const ANONYMOUS_USER_ID = "$RCAnonymousID:credits_viewer";
|
|
6
|
+
export function configureConsumablesWebAdapter(revenueCatApiKey) {
|
|
7
|
+
apiKey = revenueCatApiKey;
|
|
8
|
+
}
|
|
9
|
+
async function ensureInitialized(requireUser = false) {
|
|
10
|
+
if (!apiKey)
|
|
11
|
+
throw new Error("RevenueCat not configured");
|
|
12
|
+
if (requireUser && !currentUserId) {
|
|
13
|
+
throw new Error("RevenueCat user not set");
|
|
14
|
+
}
|
|
15
|
+
if (!purchasesInstance) {
|
|
16
|
+
const SDK = await import("@revenuecat/purchases-js");
|
|
17
|
+
purchasesInstance = SDK.Purchases.configure({
|
|
18
|
+
apiKey,
|
|
19
|
+
appUserId: currentUserId || ANONYMOUS_USER_ID,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return purchasesInstance;
|
|
23
|
+
}
|
|
24
|
+
export async function setConsumablesWebUser(userId, email) {
|
|
25
|
+
if (!apiKey)
|
|
26
|
+
return;
|
|
27
|
+
if (currentUserId === userId && purchasesInstance)
|
|
28
|
+
return;
|
|
29
|
+
if (pendingUserSetup === userId)
|
|
30
|
+
return;
|
|
31
|
+
pendingUserSetup = userId;
|
|
32
|
+
try {
|
|
33
|
+
const SDK = await import("@revenuecat/purchases-js");
|
|
34
|
+
if (purchasesInstance)
|
|
35
|
+
purchasesInstance.close();
|
|
36
|
+
purchasesInstance = SDK.Purchases.configure({
|
|
37
|
+
apiKey,
|
|
38
|
+
appUserId: userId,
|
|
39
|
+
});
|
|
40
|
+
currentUserId = userId;
|
|
41
|
+
if (email) {
|
|
42
|
+
try {
|
|
43
|
+
await purchasesInstance.setAttributes({ email });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
pendingUserSetup = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function clearConsumablesWebUser() {
|
|
54
|
+
if (purchasesInstance) {
|
|
55
|
+
purchasesInstance.close();
|
|
56
|
+
purchasesInstance = null;
|
|
57
|
+
}
|
|
58
|
+
currentUserId = null;
|
|
59
|
+
}
|
|
60
|
+
export function hasConsumablesWebUser() {
|
|
61
|
+
return currentUserId !== null && purchasesInstance !== null;
|
|
62
|
+
}
|
|
63
|
+
function convertPackage(pkg) {
|
|
64
|
+
const product = pkg.rcBillingProduct;
|
|
65
|
+
const metadata = product.metadata || {};
|
|
66
|
+
const credits = typeof metadata.credits === "number" ? metadata.credits : 0;
|
|
67
|
+
return {
|
|
68
|
+
packageId: pkg.identifier,
|
|
69
|
+
productId: product.identifier,
|
|
70
|
+
title: product.title,
|
|
71
|
+
description: product.description || null,
|
|
72
|
+
credits,
|
|
73
|
+
price: product.currentPrice?.amountMicros
|
|
74
|
+
? product.currentPrice.amountMicros / 1000000
|
|
75
|
+
: 0,
|
|
76
|
+
priceString: product.currentPrice?.formattedPrice || "$0",
|
|
77
|
+
currencyCode: product.currentPrice?.currency || "USD",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function createConsumablesWebAdapter() {
|
|
81
|
+
return {
|
|
82
|
+
async setUserId(userId, email) {
|
|
83
|
+
if (userId) {
|
|
84
|
+
await setConsumablesWebUser(userId, email);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
clearConsumablesWebUser();
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
async getOfferings() {
|
|
91
|
+
try {
|
|
92
|
+
const purchases = await ensureInitialized(false);
|
|
93
|
+
const offerings = await purchases.getOfferings();
|
|
94
|
+
const all = {};
|
|
95
|
+
for (const [key, offering] of Object.entries(offerings.all)) {
|
|
96
|
+
all[key] = {
|
|
97
|
+
identifier: offering.identifier,
|
|
98
|
+
metadata: offering.metadata || null,
|
|
99
|
+
packages: offering.availablePackages.map(convertPackage),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return { all };
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error("[consumables-web] Failed to get offerings:", error);
|
|
106
|
+
return { all: {} };
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
async purchase(params) {
|
|
110
|
+
const purchases = await ensureInitialized(true);
|
|
111
|
+
const offerings = await purchases.getOfferings();
|
|
112
|
+
let packageToPurchase;
|
|
113
|
+
for (const offering of Object.values(offerings.all)) {
|
|
114
|
+
packageToPurchase = offering.availablePackages.find((pkg) => pkg.identifier === params.packageId);
|
|
115
|
+
if (packageToPurchase)
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (!packageToPurchase) {
|
|
119
|
+
throw new Error(`Package not found: ${params.packageId}`);
|
|
120
|
+
}
|
|
121
|
+
const result = await purchases.purchase({
|
|
122
|
+
rcPackage: packageToPurchase,
|
|
123
|
+
});
|
|
124
|
+
const product = packageToPurchase.rcBillingProduct;
|
|
125
|
+
const metadata = product.metadata || {};
|
|
126
|
+
const credits = typeof metadata.credits === "number" ? metadata.credits : 0;
|
|
127
|
+
const nonSubTransactions = result.customerInfo.nonSubscriptionTransactions || [];
|
|
128
|
+
const latestTransaction = nonSubTransactions[nonSubTransactions.length - 1];
|
|
129
|
+
const transactionId = latestTransaction?.transactionIdentifier || `web_${Date.now()}`;
|
|
130
|
+
return {
|
|
131
|
+
transactionId,
|
|
132
|
+
productId: product.identifier,
|
|
133
|
+
credits,
|
|
134
|
+
priceCents: product.currentPrice?.amountMicros
|
|
135
|
+
? Math.round(product.currentPrice.amountMicros / 10000)
|
|
136
|
+
: 0,
|
|
137
|
+
currency: product.currentPrice?.currency || "USD",
|
|
138
|
+
source: "web",
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=revenuecat-web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revenuecat-web.js","sourceRoot":"","sources":["../../src/adapters/revenuecat-web.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * RevenueCat Web Adapter for Consumables\n *\n * Wraps @revenuecat/purchases-js for consumable credit purchases.\n * Lazy loads the SDK to minimize bundle size.\n */\n\nimport type {\n ConsumablePurchaseParams,\n ConsumablePurchaseResult,\n ConsumablesAdapter,\n} from \"../types/adapter\";\nimport type { CreditPackage } from \"../types\";\n\ntype Purchases = import(\"@revenuecat/purchases-js\").Purchases;\ntype Package = import(\"@revenuecat/purchases-js\").Package;\n\nlet purchasesInstance: Purchases | null = null;\nlet currentUserId: string | null = null;\nlet apiKey: string | null = null;\nlet pendingUserSetup: string | null = null;\n\nconst ANONYMOUS_USER_ID = \"$RCAnonymousID:credits_viewer\";\n\n/** Configure the web adapter with RevenueCat API key. */\nexport function configureConsumablesWebAdapter(revenueCatApiKey: string): void {\n apiKey = revenueCatApiKey;\n}\n\nasync function ensureInitialized(requireUser = false): Promise<Purchases> {\n if (!apiKey) throw new Error(\"RevenueCat not configured\");\n if (requireUser && !currentUserId) {\n throw new Error(\"RevenueCat user not set\");\n }\n\n if (!purchasesInstance) {\n const SDK = await import(\"@revenuecat/purchases-js\");\n purchasesInstance = SDK.Purchases.configure({\n apiKey,\n appUserId: currentUserId || ANONYMOUS_USER_ID,\n });\n }\n return purchasesInstance;\n}\n\n/** Set the current user for RevenueCat. */\nexport async function setConsumablesWebUser(\n userId: string,\n email?: string,\n): Promise<void> {\n if (!apiKey) return;\n if (currentUserId === userId && purchasesInstance) return;\n if (pendingUserSetup === userId) return;\n\n pendingUserSetup = userId;\n try {\n const SDK = await import(\"@revenuecat/purchases-js\");\n if (purchasesInstance) purchasesInstance.close();\n purchasesInstance = SDK.Purchases.configure({\n apiKey,\n appUserId: userId,\n });\n currentUserId = userId;\n if (email) {\n try {\n await purchasesInstance.setAttributes({ email });\n } catch {\n // Ignore attribute errors\n }\n }\n } finally {\n pendingUserSetup = null;\n }\n}\n\n/** Clear the current user (on logout). */\nexport function clearConsumablesWebUser(): void {\n if (purchasesInstance) {\n purchasesInstance.close();\n purchasesInstance = null;\n }\n currentUserId = null;\n}\n\n/** Check if a user is configured. */\nexport function hasConsumablesWebUser(): boolean {\n return currentUserId !== null && purchasesInstance !== null;\n}\n\nfunction convertPackage(pkg: Package): CreditPackage {\n const product = pkg.rcBillingProduct;\n const metadata = (product as any).metadata || {};\n const credits = typeof metadata.credits === \"number\" ? metadata.credits : 0;\n\n return {\n packageId: pkg.identifier,\n productId: product.identifier,\n title: product.title,\n description: product.description || null,\n credits,\n price: product.currentPrice?.amountMicros\n ? product.currentPrice.amountMicros / 1_000_000\n : 0,\n priceString: product.currentPrice?.formattedPrice || \"$0\",\n currencyCode: product.currentPrice?.currency || \"USD\",\n };\n}\n\n/** Create the consumables adapter for web. */\nexport function createConsumablesWebAdapter(): ConsumablesAdapter {\n return {\n async setUserId(userId: string | undefined, email?: string): Promise<void> {\n if (userId) {\n await setConsumablesWebUser(userId, email);\n } else {\n clearConsumablesWebUser();\n }\n },\n\n async getOfferings() {\n try {\n const purchases = await ensureInitialized(false);\n const offerings = await purchases.getOfferings();\n\n const all: Record<\n string,\n {\n identifier: string;\n metadata: Record<string, unknown> | null;\n packages: CreditPackage[];\n }\n > = {};\n\n for (const [key, offering] of Object.entries(offerings.all)) {\n all[key] = {\n identifier: offering.identifier,\n metadata: (offering.metadata as Record<string, unknown>) || null,\n packages: offering.availablePackages.map(convertPackage),\n };\n }\n\n return { all };\n } catch (error) {\n console.error(\"[consumables-web] Failed to get offerings:\", error);\n return { all: {} };\n }\n },\n\n async purchase(\n params: ConsumablePurchaseParams,\n ): Promise<ConsumablePurchaseResult> {\n const purchases = await ensureInitialized(true);\n const offerings = await purchases.getOfferings();\n\n let packageToPurchase: Package | undefined;\n for (const offering of Object.values(offerings.all)) {\n packageToPurchase = offering.availablePackages.find(\n (pkg) => pkg.identifier === params.packageId,\n );\n if (packageToPurchase) break;\n }\n\n if (!packageToPurchase) {\n throw new Error(`Package not found: ${params.packageId}`);\n }\n\n const result = await purchases.purchase({\n rcPackage: packageToPurchase,\n });\n\n const product = packageToPurchase.rcBillingProduct;\n const metadata = (product as any).metadata || {};\n const credits =\n typeof metadata.credits === \"number\" ? metadata.credits : 0;\n\n // Extract transaction ID from the result\n const nonSubTransactions =\n result.customerInfo.nonSubscriptionTransactions || [];\n const latestTransaction =\n nonSubTransactions[nonSubTransactions.length - 1];\n const transactionId =\n latestTransaction?.transactionIdentifier || `web_${Date.now()}`;\n\n return {\n transactionId,\n productId: product.identifier,\n credits,\n priceCents: product.currentPrice?.amountMicros\n ? Math.round(product.currentPrice.amountMicros / 10_000)\n : 0,\n currency: product.currentPrice?.currency || \"USD\",\n source: \"web\",\n };\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { ConsumablesService, type ConsumablesServiceConfig } from "./service";
|
|
2
|
+
export { initializeConsumables, getConsumablesInstance, isConsumablesInitialized, resetConsumables, refreshConsumablesBalance, setConsumablesUserId, getConsumablesUserId, onConsumablesBalanceChange, onConsumablesUserIdChange, notifyBalanceChange, type ConsumablesConfig, } from "./singleton";
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,KAAK,wBAAwB,EAAE,MAAM,WAAW,CAAC;AAC9E,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,wBAAwB,EACxB,gBAAgB,EAChB,yBAAyB,EACzB,oBAAoB,EACpB,oBAAoB,EACpB,0BAA0B,EAC1B,yBAAyB,EACzB,mBAAmB,EACnB,KAAK,iBAAiB,GACvB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { ConsumablesService } from "./service";
|
|
2
|
+
export { initializeConsumables, getConsumablesInstance, isConsumablesInitialized, resetConsumables, refreshConsumablesBalance, setConsumablesUserId, getConsumablesUserId, onConsumablesBalanceChange, onConsumablesUserIdChange, notifyBalanceChange, } from "./singleton";
|
|
3
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAiC,MAAM,WAAW,CAAC;AAC9E,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,wBAAwB,EACxB,gBAAgB,EAChB,yBAAyB,EACzB,oBAAoB,EACpB,oBAAoB,EACpB,0BAA0B,EAC1B,yBAAyB,EACzB,mBAAmB,GAEpB,MAAM,aAAa,CAAC","sourcesContent":["export { ConsumablesService, type ConsumablesServiceConfig } from \"./service\";\nexport {\n initializeConsumables,\n getConsumablesInstance,\n isConsumablesInitialized,\n resetConsumables,\n refreshConsumablesBalance,\n setConsumablesUserId,\n getConsumablesUserId,\n onConsumablesBalanceChange,\n onConsumablesUserIdChange,\n notifyBalanceChange,\n type ConsumablesConfig,\n} from \"./singleton\";\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ConsumablePurchaseParams, ConsumablesAdapter, CreditBalance, CreditOffering, CreditPurchaseRecord, CreditUsageRecord } from "../types";
|
|
2
|
+
import type { ConsumablesApiClient } from "../network/ConsumablesApiClient";
|
|
3
|
+
export interface ConsumablesServiceConfig {
|
|
4
|
+
adapter: ConsumablesAdapter;
|
|
5
|
+
apiClient: ConsumablesApiClient;
|
|
6
|
+
}
|
|
7
|
+
export declare class ConsumablesService {
|
|
8
|
+
private adapter;
|
|
9
|
+
private apiClient;
|
|
10
|
+
private balanceCache;
|
|
11
|
+
private offeringsCache;
|
|
12
|
+
private loadOfferingsPromise;
|
|
13
|
+
private isLoadingBalance;
|
|
14
|
+
constructor(config: ConsumablesServiceConfig);
|
|
15
|
+
loadOfferings(): Promise<void>;
|
|
16
|
+
getOffering(offeringId: string): CreditOffering | null;
|
|
17
|
+
getOfferingIds(): string[];
|
|
18
|
+
loadBalance(): Promise<CreditBalance>;
|
|
19
|
+
getCachedBalance(): CreditBalance | null;
|
|
20
|
+
purchase(params: ConsumablePurchaseParams): Promise<CreditBalance>;
|
|
21
|
+
recordUsage(filename?: string): Promise<{
|
|
22
|
+
balance: number;
|
|
23
|
+
success: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
getPurchaseHistory(limit?: number, offset?: number): Promise<CreditPurchaseRecord[]>;
|
|
26
|
+
getUsageHistory(limit?: number, offset?: number): Promise<CreditUsageRecord[]>;
|
|
27
|
+
clearCache(): void;
|
|
28
|
+
hasLoadedOfferings(): boolean;
|
|
29
|
+
hasLoadedBalance(): boolean;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/core/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAE5E,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,kBAAkB,CAAC;IAC5B,SAAS,EAAE,oBAAoB,CAAC;CACjC;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,cAAc,CAA0C;IAChE,OAAO,CAAC,oBAAoB,CAA8B;IAC1D,OAAO,CAAC,gBAAgB,CAAS;gBAErB,MAAM,EAAE,wBAAwB;IAMtC,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAsBpC,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAKtD,cAAc,IAAI,MAAM,EAAE;IAKpB,WAAW,IAAI,OAAO,CAAC,aAAa,CAAC;IAc3C,gBAAgB,IAAI,aAAa,GAAG,IAAI;IAKlC,QAAQ,CAAC,MAAM,EAAE,wBAAwB,GAAG,OAAO,CAAC,aAAa,CAAC;IAqBlE,WAAW,CACf,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAe3C,kBAAkB,CACtB,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAK5B,eAAe,CACnB,KAAK,CAAC,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAK/B,UAAU,IAAI,IAAI;IAKlB,kBAAkB,IAAI,OAAO;IAI7B,gBAAgB,IAAI,OAAO;CAG5B"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export class ConsumablesService {
|
|
2
|
+
constructor(config) {
|
|
3
|
+
this.balanceCache = null;
|
|
4
|
+
this.offeringsCache = new Map();
|
|
5
|
+
this.loadOfferingsPromise = null;
|
|
6
|
+
this.isLoadingBalance = false;
|
|
7
|
+
this.adapter = config.adapter;
|
|
8
|
+
this.apiClient = config.apiClient;
|
|
9
|
+
}
|
|
10
|
+
async loadOfferings() {
|
|
11
|
+
if (this.offeringsCache.size > 0)
|
|
12
|
+
return;
|
|
13
|
+
if (this.loadOfferingsPromise)
|
|
14
|
+
return this.loadOfferingsPromise;
|
|
15
|
+
this.loadOfferingsPromise = (async () => {
|
|
16
|
+
try {
|
|
17
|
+
const result = await this.adapter.getOfferings();
|
|
18
|
+
for (const [key, offering] of Object.entries(result.all)) {
|
|
19
|
+
this.offeringsCache.set(key, {
|
|
20
|
+
offeringId: offering.identifier,
|
|
21
|
+
packages: offering.packages,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
this.loadOfferingsPromise = null;
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
29
|
+
return this.loadOfferingsPromise;
|
|
30
|
+
}
|
|
31
|
+
getOffering(offeringId) {
|
|
32
|
+
return this.offeringsCache.get(offeringId) ?? null;
|
|
33
|
+
}
|
|
34
|
+
getOfferingIds() {
|
|
35
|
+
return Array.from(this.offeringsCache.keys());
|
|
36
|
+
}
|
|
37
|
+
async loadBalance() {
|
|
38
|
+
if (this.isLoadingBalance && this.balanceCache) {
|
|
39
|
+
return this.balanceCache;
|
|
40
|
+
}
|
|
41
|
+
this.isLoadingBalance = true;
|
|
42
|
+
try {
|
|
43
|
+
this.balanceCache = await this.apiClient.getBalance();
|
|
44
|
+
return this.balanceCache;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
this.isLoadingBalance = false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
getCachedBalance() {
|
|
51
|
+
return this.balanceCache;
|
|
52
|
+
}
|
|
53
|
+
async purchase(params) {
|
|
54
|
+
const purchaseResult = await this.adapter.purchase(params);
|
|
55
|
+
const balance = await this.apiClient.recordPurchase({
|
|
56
|
+
credits: purchaseResult.credits,
|
|
57
|
+
source: purchaseResult.source,
|
|
58
|
+
transaction_ref_id: purchaseResult.transactionId,
|
|
59
|
+
product_id: purchaseResult.productId,
|
|
60
|
+
price_cents: purchaseResult.priceCents,
|
|
61
|
+
currency: purchaseResult.currency,
|
|
62
|
+
});
|
|
63
|
+
this.balanceCache = balance;
|
|
64
|
+
return balance;
|
|
65
|
+
}
|
|
66
|
+
async recordUsage(filename) {
|
|
67
|
+
const result = await this.apiClient.recordUsage(filename);
|
|
68
|
+
if (this.balanceCache) {
|
|
69
|
+
this.balanceCache = {
|
|
70
|
+
...this.balanceCache,
|
|
71
|
+
balance: result.balance,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
async getPurchaseHistory(limit, offset) {
|
|
77
|
+
return this.apiClient.getPurchaseHistory(limit, offset);
|
|
78
|
+
}
|
|
79
|
+
async getUsageHistory(limit, offset) {
|
|
80
|
+
return this.apiClient.getUsageHistory(limit, offset);
|
|
81
|
+
}
|
|
82
|
+
clearCache() {
|
|
83
|
+
this.balanceCache = null;
|
|
84
|
+
}
|
|
85
|
+
hasLoadedOfferings() {
|
|
86
|
+
return this.offeringsCache.size > 0;
|
|
87
|
+
}
|
|
88
|
+
hasLoadedBalance() {
|
|
89
|
+
return this.balanceCache !== null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/core/service.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ConsumablePurchaseParams,\n ConsumablesAdapter,\n CreditBalance,\n CreditOffering,\n CreditPurchaseRecord,\n CreditUsageRecord,\n} from \"../types\";\nimport type { ConsumablesApiClient } from \"../network/ConsumablesApiClient\";\n\nexport interface ConsumablesServiceConfig {\n adapter: ConsumablesAdapter;\n apiClient: ConsumablesApiClient;\n}\n\nexport class ConsumablesService {\n private adapter: ConsumablesAdapter;\n private apiClient: ConsumablesApiClient;\n private balanceCache: CreditBalance | null = null;\n private offeringsCache: Map<string, CreditOffering> = new Map();\n private loadOfferingsPromise: Promise<void> | null = null;\n private isLoadingBalance = false;\n\n constructor(config: ConsumablesServiceConfig) {\n this.adapter = config.adapter;\n this.apiClient = config.apiClient;\n }\n\n /** Load offerings from RevenueCat (with caching). */\n async loadOfferings(): Promise<void> {\n if (this.offeringsCache.size > 0) return;\n if (this.loadOfferingsPromise) return this.loadOfferingsPromise;\n\n this.loadOfferingsPromise = (async () => {\n try {\n const result = await this.adapter.getOfferings();\n for (const [key, offering] of Object.entries(result.all)) {\n this.offeringsCache.set(key, {\n offeringId: offering.identifier,\n packages: offering.packages,\n });\n }\n } finally {\n this.loadOfferingsPromise = null;\n }\n })();\n\n return this.loadOfferingsPromise;\n }\n\n /** Get a specific offering by ID. */\n getOffering(offeringId: string): CreditOffering | null {\n return this.offeringsCache.get(offeringId) ?? null;\n }\n\n /** Get all offering IDs. */\n getOfferingIds(): string[] {\n return Array.from(this.offeringsCache.keys());\n }\n\n /** Load balance from API. */\n async loadBalance(): Promise<CreditBalance> {\n if (this.isLoadingBalance && this.balanceCache) {\n return this.balanceCache;\n }\n this.isLoadingBalance = true;\n try {\n this.balanceCache = await this.apiClient.getBalance();\n return this.balanceCache;\n } finally {\n this.isLoadingBalance = false;\n }\n }\n\n /** Get cached balance (null if not loaded). */\n getCachedBalance(): CreditBalance | null {\n return this.balanceCache;\n }\n\n /** Execute purchase: adapter.purchase() then apiClient.recordPurchase(). */\n async purchase(params: ConsumablePurchaseParams): Promise<CreditBalance> {\n // 1. Call adapter.purchase() — opens RevenueCat payment UI\n const purchaseResult = await this.adapter.purchase(params);\n\n // 2. Record on backend\n const balance = await this.apiClient.recordPurchase({\n credits: purchaseResult.credits,\n source: purchaseResult.source,\n transaction_ref_id: purchaseResult.transactionId,\n product_id: purchaseResult.productId,\n price_cents: purchaseResult.priceCents,\n currency: purchaseResult.currency,\n });\n\n // 3. Update cache\n this.balanceCache = balance;\n\n return balance;\n }\n\n /** Record a usage (download). */\n async recordUsage(\n filename?: string,\n ): Promise<{ balance: number; success: boolean }> {\n const result = await this.apiClient.recordUsage(filename);\n\n // Update cache\n if (this.balanceCache) {\n this.balanceCache = {\n ...this.balanceCache,\n balance: result.balance,\n };\n }\n\n return result;\n }\n\n /** Get purchase history from API. */\n async getPurchaseHistory(\n limit?: number,\n offset?: number,\n ): Promise<CreditPurchaseRecord[]> {\n return this.apiClient.getPurchaseHistory(limit, offset);\n }\n\n /** Get usage history from API. */\n async getUsageHistory(\n limit?: number,\n offset?: number,\n ): Promise<CreditUsageRecord[]> {\n return this.apiClient.getUsageHistory(limit, offset);\n }\n\n /** Clear cached data (on user change). */\n clearCache(): void {\n this.balanceCache = null;\n // Preserve offerings cache — products don't change per user\n }\n\n hasLoadedOfferings(): boolean {\n return this.offeringsCache.size > 0;\n }\n\n hasLoadedBalance(): boolean {\n return this.balanceCache !== null;\n }\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ConsumablesAdapter } from "../types/adapter";
|
|
2
|
+
import type { ConsumablesApiClient } from "../network/ConsumablesApiClient";
|
|
3
|
+
import { ConsumablesService } from "./service";
|
|
4
|
+
export interface ConsumablesConfig {
|
|
5
|
+
adapter: ConsumablesAdapter;
|
|
6
|
+
apiClient: ConsumablesApiClient;
|
|
7
|
+
}
|
|
8
|
+
export declare function initializeConsumables(config: ConsumablesConfig): void;
|
|
9
|
+
export declare function getConsumablesInstance(): ConsumablesService;
|
|
10
|
+
export declare function isConsumablesInitialized(): boolean;
|
|
11
|
+
export declare function resetConsumables(): void;
|
|
12
|
+
export declare function refreshConsumablesBalance(): Promise<void>;
|
|
13
|
+
export declare function setConsumablesUserId(userId: string | undefined, email?: string): Promise<void>;
|
|
14
|
+
export declare function getConsumablesUserId(): string | undefined;
|
|
15
|
+
export declare function onConsumablesBalanceChange(listener: () => void): () => void;
|
|
16
|
+
export declare function onConsumablesUserIdChange(listener: () => void): () => void;
|
|
17
|
+
export declare function notifyBalanceChange(): void;
|
|
18
|
+
//# sourceMappingURL=singleton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"singleton.d.ts","sourceRoot":"","sources":["../../src/core/singleton.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,kBAAkB,CAAC;IAC5B,SAAS,EAAE,oBAAoB,CAAC;CACjC;AASD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAMrE;AAGD,wBAAgB,sBAAsB,IAAI,kBAAkB,CAO3D;AAGD,wBAAgB,wBAAwB,IAAI,OAAO,CAElD;AAGD,wBAAgB,gBAAgB,IAAI,IAAI,CAIvC;AAGD,wBAAsB,yBAAyB,IAAI,OAAO,CAAC,IAAI,CAAC,CAM/D;AAMD,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAgBf;AAGD,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,SAAS,CAEzD;AAGD,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAM3E;AAGD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAM1E;AAGD,wBAAgB,mBAAmB,IAAI,IAAI,CAI1C"}
|