@sudobility/subscription_lib 0.0.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/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +5 -0
- package/dist/core/service.d.ts +74 -0
- package/dist/core/service.d.ts.map +1 -0
- package/dist/core/service.js +251 -0
- package/dist/core/singleton.d.ts +65 -0
- package/dist/core/singleton.d.ts.map +1 -0
- package/dist/core/singleton.js +73 -0
- package/dist/hooks/index.d.ts +9 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +8 -0
- package/dist/hooks/useSubscribable.d.ts +50 -0
- package/dist/hooks/useSubscribable.d.ts.map +1 -0
- package/dist/hooks/useSubscribable.js +80 -0
- package/dist/hooks/useSubscriptionForPeriod.d.ts +48 -0
- package/dist/hooks/useSubscriptionForPeriod.d.ts.map +1 -0
- package/dist/hooks/useSubscriptionForPeriod.js +59 -0
- package/dist/hooks/useSubscriptionPeriods.d.ts +38 -0
- package/dist/hooks/useSubscriptionPeriods.d.ts.map +1 -0
- package/dist/hooks/useSubscriptionPeriods.js +44 -0
- package/dist/hooks/useSubscriptions.d.ts +43 -0
- package/dist/hooks/useSubscriptions.d.ts.map +1 -0
- package/dist/hooks/useSubscriptions.js +80 -0
- package/dist/hooks/useUserSubscription.d.ts +39 -0
- package/dist/hooks/useUserSubscription.d.ts.map +1 -0
- package/dist/hooks/useUserSubscription.js +76 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/types/adapter.d.ts +149 -0
- package/dist/types/adapter.d.ts.map +1 -0
- package/dist/types/adapter.js +7 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +4 -0
- package/dist/types/period.d.ts +18 -0
- package/dist/types/period.d.ts.map +1 -0
- package/dist/types/period.js +25 -0
- package/dist/types/subscription.d.ts +95 -0
- package/dist/types/subscription.d.ts.map +1 -0
- package/dist/types/subscription.js +6 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/level-calculator.d.ts +68 -0
- package/dist/utils/level-calculator.d.ts.map +1 -0
- package/dist/utils/level-calculator.js +164 -0
- package/dist/utils/period-parser.d.ts +45 -0
- package/dist/utils/period-parser.d.ts.map +1 -0
- package/dist/utils/period-parser.js +113 -0
- package/package.json +55 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Exports
|
|
3
|
+
*/
|
|
4
|
+
export { SubscriptionService, type SubscriptionServiceConfig } from './service';
|
|
5
|
+
export { initializeSubscription, getSubscriptionInstance, isSubscriptionInitialized, resetSubscription, refreshSubscription, type SubscriptionConfig, } from './singleton';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,mBAAmB,EAAE,KAAK,yBAAyB,EAAE,MAAM,WAAW,CAAC;AAEhF,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,yBAAyB,EACzB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Service
|
|
3
|
+
*
|
|
4
|
+
* Core service for managing subscription data with clean abstractions.
|
|
5
|
+
*/
|
|
6
|
+
import type { AdapterPurchaseParams, AdapterPurchaseResult, SubscriptionAdapter } from '../types/adapter';
|
|
7
|
+
import type { CurrentSubscription, FreeTierConfig, SubscriptionOffer, SubscriptionPackage } from '../types/subscription';
|
|
8
|
+
/**
|
|
9
|
+
* Configuration for SubscriptionService
|
|
10
|
+
*/
|
|
11
|
+
export interface SubscriptionServiceConfig {
|
|
12
|
+
/** RevenueCat adapter implementation */
|
|
13
|
+
adapter: SubscriptionAdapter;
|
|
14
|
+
/** Free tier configuration */
|
|
15
|
+
freeTier: FreeTierConfig;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Service for managing subscription data with clean abstractions.
|
|
19
|
+
*/
|
|
20
|
+
export declare class SubscriptionService {
|
|
21
|
+
private adapter;
|
|
22
|
+
private freeTier;
|
|
23
|
+
private offersCache;
|
|
24
|
+
private currentSubscription;
|
|
25
|
+
private isLoadingOfferings;
|
|
26
|
+
private isLoadingCustomerInfo;
|
|
27
|
+
constructor(config: SubscriptionServiceConfig);
|
|
28
|
+
/**
|
|
29
|
+
* Load offerings from RevenueCat
|
|
30
|
+
*/
|
|
31
|
+
loadOfferings(params?: {
|
|
32
|
+
currency?: string;
|
|
33
|
+
}): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Load customer info from RevenueCat
|
|
36
|
+
*/
|
|
37
|
+
loadCustomerInfo(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Get offer by ID with complete hierarchy
|
|
40
|
+
*/
|
|
41
|
+
getOffer(offerId: string): SubscriptionOffer | null;
|
|
42
|
+
/**
|
|
43
|
+
* Get all offer IDs
|
|
44
|
+
*/
|
|
45
|
+
getOfferIds(): string[];
|
|
46
|
+
/**
|
|
47
|
+
* Get current subscription info
|
|
48
|
+
*/
|
|
49
|
+
getCurrentSubscription(): CurrentSubscription | null;
|
|
50
|
+
/**
|
|
51
|
+
* Get free tier as a SubscriptionPackage
|
|
52
|
+
*/
|
|
53
|
+
getFreeTierPackage(): SubscriptionPackage;
|
|
54
|
+
/**
|
|
55
|
+
* Get free tier config
|
|
56
|
+
*/
|
|
57
|
+
getFreeTierConfig(): FreeTierConfig;
|
|
58
|
+
/**
|
|
59
|
+
* Make a purchase
|
|
60
|
+
*/
|
|
61
|
+
purchase(params: AdapterPurchaseParams): Promise<AdapterPurchaseResult>;
|
|
62
|
+
/**
|
|
63
|
+
* Check if offerings are loaded
|
|
64
|
+
*/
|
|
65
|
+
hasLoadedOfferings(): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Check if customer info is loaded
|
|
68
|
+
*/
|
|
69
|
+
hasLoadedCustomerInfo(): boolean;
|
|
70
|
+
private parseOffering;
|
|
71
|
+
private parsePackage;
|
|
72
|
+
private extractEntitlementsFromMetadata;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/core/service.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAGV,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EACV,mBAAmB,EACnB,cAAc,EACd,iBAAiB,EACjB,mBAAmB,EAEpB,MAAM,uBAAuB,CAAC;AAG/B;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,wCAAwC;IACxC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,8BAA8B;IAC9B,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED;;GAEG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,WAAW,CAA6C;IAChE,OAAO,CAAC,mBAAmB,CAAoC;IAC/D,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,qBAAqB,CAAS;gBAE1B,MAAM,EAAE,yBAAyB;IAS7C;;OAEG;IACG,aAAa,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBlE;;OAEG;IACG,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IA6DvC;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAInD;;OAEG;IACH,WAAW,IAAI,MAAM,EAAE;IAIvB;;OAEG;IACH,sBAAsB,IAAI,mBAAmB,GAAG,IAAI;IAIpD;;OAEG;IACH,kBAAkB,IAAI,mBAAmB;IASzC;;OAEG;IACH,iBAAiB,IAAI,cAAc;IAInC;;OAEG;IACG,QAAQ,CACZ,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IASjC;;OAEG;IACH,kBAAkB,IAAI,OAAO;IAI7B;;OAEG;IACH,qBAAqB,IAAI,OAAO;IAQhC,OAAO,CAAC,aAAa;IAsBrB,OAAO,CAAC,YAAY;IAgCpB,OAAO,CAAC,+BAA+B;CA6BxC"}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Service
|
|
3
|
+
*
|
|
4
|
+
* Core service for managing subscription data with clean abstractions.
|
|
5
|
+
*/
|
|
6
|
+
import { parseISO8601Period } from '../utils/period-parser';
|
|
7
|
+
/**
|
|
8
|
+
* Service for managing subscription data with clean abstractions.
|
|
9
|
+
*/
|
|
10
|
+
export class SubscriptionService {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
Object.defineProperty(this, "adapter", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
writable: true,
|
|
16
|
+
value: void 0
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(this, "freeTier", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
writable: true,
|
|
22
|
+
value: void 0
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(this, "offersCache", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
configurable: true,
|
|
27
|
+
writable: true,
|
|
28
|
+
value: new Map()
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(this, "currentSubscription", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
configurable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
value: null
|
|
35
|
+
});
|
|
36
|
+
Object.defineProperty(this, "isLoadingOfferings", {
|
|
37
|
+
enumerable: true,
|
|
38
|
+
configurable: true,
|
|
39
|
+
writable: true,
|
|
40
|
+
value: false
|
|
41
|
+
});
|
|
42
|
+
Object.defineProperty(this, "isLoadingCustomerInfo", {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
writable: true,
|
|
46
|
+
value: false
|
|
47
|
+
});
|
|
48
|
+
this.adapter = config.adapter;
|
|
49
|
+
this.freeTier = config.freeTier;
|
|
50
|
+
}
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Data Loading
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
/**
|
|
55
|
+
* Load offerings from RevenueCat
|
|
56
|
+
*/
|
|
57
|
+
async loadOfferings(params) {
|
|
58
|
+
if (this.isLoadingOfferings)
|
|
59
|
+
return;
|
|
60
|
+
this.isLoadingOfferings = true;
|
|
61
|
+
try {
|
|
62
|
+
const offerings = await this.adapter.getOfferings(params);
|
|
63
|
+
this.offersCache.clear();
|
|
64
|
+
for (const [offerId, offering] of Object.entries(offerings.all)) {
|
|
65
|
+
const parsedOffer = this.parseOffering(offerId, offering);
|
|
66
|
+
this.offersCache.set(offerId, parsedOffer);
|
|
67
|
+
}
|
|
68
|
+
// Also add current offering as 'default' if available
|
|
69
|
+
if (offerings.current && !this.offersCache.has('default')) {
|
|
70
|
+
const parsedOffer = this.parseOffering('default', offerings.current);
|
|
71
|
+
this.offersCache.set('default', parsedOffer);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
this.isLoadingOfferings = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Load customer info from RevenueCat
|
|
80
|
+
*/
|
|
81
|
+
async loadCustomerInfo() {
|
|
82
|
+
if (this.isLoadingCustomerInfo)
|
|
83
|
+
return;
|
|
84
|
+
this.isLoadingCustomerInfo = true;
|
|
85
|
+
try {
|
|
86
|
+
// Ensure offerings are loaded first so we can match packageId
|
|
87
|
+
if (!this.hasLoadedOfferings()) {
|
|
88
|
+
await this.loadOfferings();
|
|
89
|
+
}
|
|
90
|
+
const customerInfo = await this.adapter.getCustomerInfo();
|
|
91
|
+
const activeEntitlementIds = Object.keys(customerInfo.entitlements.active);
|
|
92
|
+
if (activeEntitlementIds.length === 0) {
|
|
93
|
+
this.currentSubscription = {
|
|
94
|
+
isActive: false,
|
|
95
|
+
entitlements: [],
|
|
96
|
+
};
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const firstEntitlement = customerInfo.entitlements.active[activeEntitlementIds[0]];
|
|
100
|
+
// Find the package for this product
|
|
101
|
+
let packageId;
|
|
102
|
+
let period;
|
|
103
|
+
for (const offer of this.offersCache.values()) {
|
|
104
|
+
const pkg = offer.packages.find(p => p.product?.productId === firstEntitlement.productIdentifier);
|
|
105
|
+
if (pkg) {
|
|
106
|
+
packageId = pkg.packageId;
|
|
107
|
+
period = pkg.product?.period;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
this.currentSubscription = {
|
|
112
|
+
isActive: true,
|
|
113
|
+
productId: firstEntitlement.productIdentifier,
|
|
114
|
+
packageId,
|
|
115
|
+
entitlements: activeEntitlementIds,
|
|
116
|
+
period,
|
|
117
|
+
expirationDate: firstEntitlement.expirationDate
|
|
118
|
+
? new Date(firstEntitlement.expirationDate)
|
|
119
|
+
: undefined,
|
|
120
|
+
willRenew: firstEntitlement.willRenew,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
this.isLoadingCustomerInfo = false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Public API
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
/**
|
|
131
|
+
* Get offer by ID with complete hierarchy
|
|
132
|
+
*/
|
|
133
|
+
getOffer(offerId) {
|
|
134
|
+
return this.offersCache.get(offerId) ?? null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get all offer IDs
|
|
138
|
+
*/
|
|
139
|
+
getOfferIds() {
|
|
140
|
+
return Array.from(this.offersCache.keys());
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get current subscription info
|
|
144
|
+
*/
|
|
145
|
+
getCurrentSubscription() {
|
|
146
|
+
return this.currentSubscription;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get free tier as a SubscriptionPackage
|
|
150
|
+
*/
|
|
151
|
+
getFreeTierPackage() {
|
|
152
|
+
return {
|
|
153
|
+
packageId: this.freeTier.packageId,
|
|
154
|
+
name: this.freeTier.name,
|
|
155
|
+
product: undefined,
|
|
156
|
+
entitlements: [],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get free tier config
|
|
161
|
+
*/
|
|
162
|
+
getFreeTierConfig() {
|
|
163
|
+
return this.freeTier;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Make a purchase
|
|
167
|
+
*/
|
|
168
|
+
async purchase(params) {
|
|
169
|
+
const result = await this.adapter.purchase(params);
|
|
170
|
+
// Reload customer info after purchase
|
|
171
|
+
await this.loadCustomerInfo();
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Check if offerings are loaded
|
|
176
|
+
*/
|
|
177
|
+
hasLoadedOfferings() {
|
|
178
|
+
return this.offersCache.size > 0;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Check if customer info is loaded
|
|
182
|
+
*/
|
|
183
|
+
hasLoadedCustomerInfo() {
|
|
184
|
+
return this.currentSubscription !== null;
|
|
185
|
+
}
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Private Helpers
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
parseOffering(offerId, offering) {
|
|
190
|
+
const packages = [];
|
|
191
|
+
// Extract entitlements from metadata if available
|
|
192
|
+
const metadataEntitlements = this.extractEntitlementsFromMetadata(offering.metadata);
|
|
193
|
+
for (const pkg of offering.availablePackages) {
|
|
194
|
+
packages.push(this.parsePackage(pkg, metadataEntitlements));
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
offerId,
|
|
198
|
+
metadata: offering.metadata ?? undefined,
|
|
199
|
+
packages,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
parsePackage(pkg, metadataEntitlements) {
|
|
203
|
+
const product = pkg.product;
|
|
204
|
+
const defaultOption = product.subscriptionOptions
|
|
205
|
+
? Object.values(product.subscriptionOptions)[0]
|
|
206
|
+
: undefined;
|
|
207
|
+
const parsedProduct = {
|
|
208
|
+
productId: product.identifier,
|
|
209
|
+
name: product.title,
|
|
210
|
+
description: product.description ?? undefined,
|
|
211
|
+
price: product.price,
|
|
212
|
+
priceString: product.priceString,
|
|
213
|
+
currency: product.currencyCode,
|
|
214
|
+
period: parseISO8601Period(product.normalPeriodDuration),
|
|
215
|
+
periodDuration: product.normalPeriodDuration ?? '',
|
|
216
|
+
trialPeriod: defaultOption?.trial?.periodDuration ?? undefined,
|
|
217
|
+
introPrice: defaultOption?.introPrice?.priceString ?? undefined,
|
|
218
|
+
introPricePeriod: defaultOption?.introPrice?.periodDuration ?? undefined,
|
|
219
|
+
introPriceCycles: defaultOption?.introPrice?.cycleCount,
|
|
220
|
+
};
|
|
221
|
+
return {
|
|
222
|
+
packageId: pkg.identifier,
|
|
223
|
+
name: product.title,
|
|
224
|
+
product: parsedProduct,
|
|
225
|
+
entitlements: metadataEntitlements,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
extractEntitlementsFromMetadata(metadata) {
|
|
229
|
+
if (!metadata)
|
|
230
|
+
return [];
|
|
231
|
+
const entitlements = [];
|
|
232
|
+
// Single entitlement in metadata
|
|
233
|
+
if (metadata.entitlement && typeof metadata.entitlement === 'string') {
|
|
234
|
+
entitlements.push(metadata.entitlement);
|
|
235
|
+
}
|
|
236
|
+
// Array of entitlements in metadata
|
|
237
|
+
if (Array.isArray(metadata.entitlements)) {
|
|
238
|
+
for (const ent of metadata.entitlements) {
|
|
239
|
+
if (typeof ent === 'string') {
|
|
240
|
+
entitlements.push(ent);
|
|
241
|
+
}
|
|
242
|
+
else if (typeof ent === 'object' &&
|
|
243
|
+
ent !== null &&
|
|
244
|
+
typeof ent.identifier === 'string') {
|
|
245
|
+
entitlements.push(ent.identifier);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return entitlements;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Singleton
|
|
3
|
+
*
|
|
4
|
+
* Global singleton manager for the SubscriptionService.
|
|
5
|
+
*/
|
|
6
|
+
import type { SubscriptionAdapter } from '../types/adapter';
|
|
7
|
+
import type { FreeTierConfig } from '../types/subscription';
|
|
8
|
+
import { SubscriptionService } from './service';
|
|
9
|
+
/**
|
|
10
|
+
* Configuration for initializing the subscription singleton
|
|
11
|
+
*/
|
|
12
|
+
export interface SubscriptionConfig {
|
|
13
|
+
/** RevenueCat adapter implementation */
|
|
14
|
+
adapter: SubscriptionAdapter;
|
|
15
|
+
/** Free tier configuration */
|
|
16
|
+
freeTier: FreeTierConfig;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Initialize the subscription singleton
|
|
20
|
+
*
|
|
21
|
+
* Call this once at app startup with your RevenueCat adapter.
|
|
22
|
+
*
|
|
23
|
+
* @param config Configuration with adapter and free tier info
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import { initializeSubscription } from '@sudobility/subscription_lib';
|
|
28
|
+
* import { createWebRevenueCatAdapter } from './adapters/web';
|
|
29
|
+
*
|
|
30
|
+
* initializeSubscription({
|
|
31
|
+
* adapter: createWebRevenueCatAdapter(purchases),
|
|
32
|
+
* freeTier: { packageId: 'free', name: 'Free' }
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function initializeSubscription(config: SubscriptionConfig): void;
|
|
37
|
+
/**
|
|
38
|
+
* Get the subscription service singleton
|
|
39
|
+
*
|
|
40
|
+
* @throws Error if not initialized
|
|
41
|
+
*/
|
|
42
|
+
export declare function getSubscriptionInstance(): SubscriptionService;
|
|
43
|
+
/**
|
|
44
|
+
* Check if subscription singleton is initialized
|
|
45
|
+
*/
|
|
46
|
+
export declare function isSubscriptionInitialized(): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Reset the subscription singleton (mainly for testing)
|
|
49
|
+
*/
|
|
50
|
+
export declare function resetSubscription(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Refresh subscription data (customer info and offerings)
|
|
53
|
+
*
|
|
54
|
+
* Call this after a purchase to ensure subscription state is up to date.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* import { refreshSubscription } from '@sudobility/subscription_lib';
|
|
59
|
+
*
|
|
60
|
+
* // After purchase completes
|
|
61
|
+
* await refreshSubscription();
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare function refreshSubscription(): Promise<void>;
|
|
65
|
+
//# sourceMappingURL=singleton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"singleton.d.ts","sourceRoot":"","sources":["../../src/core/singleton.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAEhD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,wCAAwC;IACxC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,8BAA8B;IAC9B,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAID;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAKvE;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,mBAAmB,CAO7D;AAED;;GAEG;AACH,wBAAgB,yBAAyB,IAAI,OAAO,CAEnD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAKzD"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Singleton
|
|
3
|
+
*
|
|
4
|
+
* Global singleton manager for the SubscriptionService.
|
|
5
|
+
*/
|
|
6
|
+
import { SubscriptionService } from './service';
|
|
7
|
+
let instance = null;
|
|
8
|
+
/**
|
|
9
|
+
* Initialize the subscription singleton
|
|
10
|
+
*
|
|
11
|
+
* Call this once at app startup with your RevenueCat adapter.
|
|
12
|
+
*
|
|
13
|
+
* @param config Configuration with adapter and free tier info
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { initializeSubscription } from '@sudobility/subscription_lib';
|
|
18
|
+
* import { createWebRevenueCatAdapter } from './adapters/web';
|
|
19
|
+
*
|
|
20
|
+
* initializeSubscription({
|
|
21
|
+
* adapter: createWebRevenueCatAdapter(purchases),
|
|
22
|
+
* freeTier: { packageId: 'free', name: 'Free' }
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function initializeSubscription(config) {
|
|
27
|
+
instance = new SubscriptionService({
|
|
28
|
+
adapter: config.adapter,
|
|
29
|
+
freeTier: config.freeTier,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get the subscription service singleton
|
|
34
|
+
*
|
|
35
|
+
* @throws Error if not initialized
|
|
36
|
+
*/
|
|
37
|
+
export function getSubscriptionInstance() {
|
|
38
|
+
if (!instance) {
|
|
39
|
+
throw new Error('Subscription not initialized. Call initializeSubscription() first.');
|
|
40
|
+
}
|
|
41
|
+
return instance;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if subscription singleton is initialized
|
|
45
|
+
*/
|
|
46
|
+
export function isSubscriptionInitialized() {
|
|
47
|
+
return instance !== null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Reset the subscription singleton (mainly for testing)
|
|
51
|
+
*/
|
|
52
|
+
export function resetSubscription() {
|
|
53
|
+
instance = null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Refresh subscription data (customer info and offerings)
|
|
57
|
+
*
|
|
58
|
+
* Call this after a purchase to ensure subscription state is up to date.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* import { refreshSubscription } from '@sudobility/subscription_lib';
|
|
63
|
+
*
|
|
64
|
+
* // After purchase completes
|
|
65
|
+
* await refreshSubscription();
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export async function refreshSubscription() {
|
|
69
|
+
if (!instance) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
await Promise.all([instance.loadOfferings(), instance.loadCustomerInfo()]);
|
|
73
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Exports
|
|
3
|
+
*/
|
|
4
|
+
export { useSubscriptions, type UseSubscriptionsResult, } from './useSubscriptions';
|
|
5
|
+
export { useUserSubscription, type UseUserSubscriptionResult, } from './useUserSubscription';
|
|
6
|
+
export { useSubscriptionPeriods, type UseSubscriptionPeriodsResult, } from './useSubscriptionPeriods';
|
|
7
|
+
export { useSubscriptionForPeriod, type UseSubscriptionForPeriodResult, } from './useSubscriptionForPeriod';
|
|
8
|
+
export { useSubscribable, type UseSubscribableResult } from './useSubscribable';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,gBAAgB,EAChB,KAAK,sBAAsB,GAC5B,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,mBAAmB,EACnB,KAAK,yBAAyB,GAC/B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,sBAAsB,EACtB,KAAK,4BAA4B,GAClC,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,wBAAwB,EACxB,KAAK,8BAA8B,GACpC,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,eAAe,EAAE,KAAK,qBAAqB,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Exports
|
|
3
|
+
*/
|
|
4
|
+
export { useSubscriptions, } from './useSubscriptions';
|
|
5
|
+
export { useUserSubscription, } from './useUserSubscription';
|
|
6
|
+
export { useSubscriptionPeriods, } from './useSubscriptionPeriods';
|
|
7
|
+
export { useSubscriptionForPeriod, } from './useSubscriptionForPeriod';
|
|
8
|
+
export { useSubscribable } from './useSubscribable';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSubscribable Hook
|
|
3
|
+
*
|
|
4
|
+
* Get packages that the user can subscribe to or upgrade to.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Result of useSubscribable hook
|
|
8
|
+
*/
|
|
9
|
+
export interface UseSubscribableResult {
|
|
10
|
+
/** Package IDs that the user can subscribe/upgrade to */
|
|
11
|
+
subscribablePackageIds: string[];
|
|
12
|
+
/** Whether data is being loaded */
|
|
13
|
+
isLoading: boolean;
|
|
14
|
+
/** Error if loading failed */
|
|
15
|
+
error: Error | null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Hook to get packages that the user can subscribe to or upgrade to
|
|
19
|
+
*
|
|
20
|
+
* Upgrade eligibility rules:
|
|
21
|
+
* - If no current subscription: all packages are subscribable
|
|
22
|
+
* - If on free tier: all paid packages are subscribable
|
|
23
|
+
* - If has subscription:
|
|
24
|
+
* - Package period must be >= current period (monthly → yearly OK, yearly → monthly NOT OK)
|
|
25
|
+
* - Package level must be >= current level (basic → pro OK, pro → basic NOT OK)
|
|
26
|
+
* - Level is determined by price comparison within the same period
|
|
27
|
+
*
|
|
28
|
+
* @param offerId Offer identifier
|
|
29
|
+
* @returns List of subscribable package IDs
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const { subscribablePackageIds } = useSubscribable('default');
|
|
34
|
+
* const { packages } = useSubscriptionForPeriod('default', 'monthly');
|
|
35
|
+
*
|
|
36
|
+
* return (
|
|
37
|
+
* <div>
|
|
38
|
+
* {packages.map(pkg => (
|
|
39
|
+
* <SubscriptionTile
|
|
40
|
+
* key={pkg.packageId}
|
|
41
|
+
* enabled={subscribablePackageIds.includes(pkg.packageId)}
|
|
42
|
+
* {...pkg}
|
|
43
|
+
* />
|
|
44
|
+
* ))}
|
|
45
|
+
* </div>
|
|
46
|
+
* );
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function useSubscribable(offerId: string): UseSubscribableResult;
|
|
50
|
+
//# sourceMappingURL=useSubscribable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSubscribable.d.ts","sourceRoot":"","sources":["../../src/hooks/useSubscribable.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,yDAAyD;IACzD,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC,mCAAmC;IACnC,SAAS,EAAE,OAAO,CAAC;IACnB,8BAA8B;IAC9B,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,CAsDtE"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSubscribable Hook
|
|
3
|
+
*
|
|
4
|
+
* Get packages that the user can subscribe to or upgrade to.
|
|
5
|
+
*/
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
|
+
import { useSubscriptions } from './useSubscriptions';
|
|
8
|
+
import { useUserSubscription } from './useUserSubscription';
|
|
9
|
+
import { findUpgradeablePackages } from '../utils/level-calculator';
|
|
10
|
+
import { getSubscriptionInstance, isSubscriptionInitialized, } from '../core/singleton';
|
|
11
|
+
/**
|
|
12
|
+
* Hook to get packages that the user can subscribe to or upgrade to
|
|
13
|
+
*
|
|
14
|
+
* Upgrade eligibility rules:
|
|
15
|
+
* - If no current subscription: all packages are subscribable
|
|
16
|
+
* - If on free tier: all paid packages are subscribable
|
|
17
|
+
* - If has subscription:
|
|
18
|
+
* - Package period must be >= current period (monthly → yearly OK, yearly → monthly NOT OK)
|
|
19
|
+
* - Package level must be >= current level (basic → pro OK, pro → basic NOT OK)
|
|
20
|
+
* - Level is determined by price comparison within the same period
|
|
21
|
+
*
|
|
22
|
+
* @param offerId Offer identifier
|
|
23
|
+
* @returns List of subscribable package IDs
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const { subscribablePackageIds } = useSubscribable('default');
|
|
28
|
+
* const { packages } = useSubscriptionForPeriod('default', 'monthly');
|
|
29
|
+
*
|
|
30
|
+
* return (
|
|
31
|
+
* <div>
|
|
32
|
+
* {packages.map(pkg => (
|
|
33
|
+
* <SubscriptionTile
|
|
34
|
+
* key={pkg.packageId}
|
|
35
|
+
* enabled={subscribablePackageIds.includes(pkg.packageId)}
|
|
36
|
+
* {...pkg}
|
|
37
|
+
* />
|
|
38
|
+
* ))}
|
|
39
|
+
* </div>
|
|
40
|
+
* );
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function useSubscribable(offerId) {
|
|
44
|
+
const { offer, isLoading: loadingOffer, error: offerError, } = useSubscriptions(offerId);
|
|
45
|
+
const { subscription, isLoading: loadingSub, error: subError, } = useUserSubscription();
|
|
46
|
+
const isLoading = loadingOffer || loadingSub;
|
|
47
|
+
const error = offerError || subError;
|
|
48
|
+
const subscribablePackageIds = useMemo(() => {
|
|
49
|
+
if (!offer) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
// Get all packages including free tier
|
|
53
|
+
let allPackages = [...offer.packages];
|
|
54
|
+
// Add free tier if initialized
|
|
55
|
+
if (isSubscriptionInitialized()) {
|
|
56
|
+
const service = getSubscriptionInstance();
|
|
57
|
+
const freeTier = service.getFreeTierPackage();
|
|
58
|
+
// Only add if not already in the list
|
|
59
|
+
if (!allPackages.some(p => p.packageId === freeTier.packageId)) {
|
|
60
|
+
allPackages = [freeTier, ...allPackages];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// No subscription or inactive = all packages subscribable
|
|
64
|
+
if (!subscription || !subscription.isActive) {
|
|
65
|
+
return allPackages.map(p => p.packageId);
|
|
66
|
+
}
|
|
67
|
+
// Pass both packageId and productId for matching
|
|
68
|
+
const current = {
|
|
69
|
+
packageId: subscription.packageId,
|
|
70
|
+
productId: subscription.productId,
|
|
71
|
+
};
|
|
72
|
+
// If we don't know the current package or product, return all (shouldn't happen)
|
|
73
|
+
if (!current.packageId && !current.productId) {
|
|
74
|
+
return allPackages.map(p => p.packageId);
|
|
75
|
+
}
|
|
76
|
+
// Calculate upgradeable packages
|
|
77
|
+
return findUpgradeablePackages(current, allPackages);
|
|
78
|
+
}, [offer, subscription]);
|
|
79
|
+
return { subscribablePackageIds, isLoading, error };
|
|
80
|
+
}
|