@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,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Period Types
|
|
3
|
+
*
|
|
4
|
+
* Standard subscription periods and utilities for period comparison.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Standard subscription periods
|
|
8
|
+
*/
|
|
9
|
+
export type SubscriptionPeriod = 'weekly' | 'monthly' | 'quarterly' | 'yearly' | 'lifetime';
|
|
10
|
+
/**
|
|
11
|
+
* Period ranking for comparison (higher = longer)
|
|
12
|
+
*/
|
|
13
|
+
export declare const PERIOD_RANKS: Record<SubscriptionPeriod, number>;
|
|
14
|
+
/**
|
|
15
|
+
* All supported periods in order
|
|
16
|
+
*/
|
|
17
|
+
export declare const ALL_PERIODS: SubscriptionPeriod[];
|
|
18
|
+
//# sourceMappingURL=period.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"period.d.ts","sourceRoot":"","sources":["../../src/types/period.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAC1B,QAAQ,GACR,SAAS,GACT,WAAW,GACX,QAAQ,GACR,UAAU,CAAC;AAEf;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,kBAAkB,EAAE,MAAM,CAM3D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,EAAE,kBAAkB,EAM3C,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Period Types
|
|
3
|
+
*
|
|
4
|
+
* Standard subscription periods and utilities for period comparison.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Period ranking for comparison (higher = longer)
|
|
8
|
+
*/
|
|
9
|
+
export const PERIOD_RANKS = {
|
|
10
|
+
weekly: 1,
|
|
11
|
+
monthly: 2,
|
|
12
|
+
quarterly: 3,
|
|
13
|
+
yearly: 4,
|
|
14
|
+
lifetime: 5,
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* All supported periods in order
|
|
18
|
+
*/
|
|
19
|
+
export const ALL_PERIODS = [
|
|
20
|
+
'weekly',
|
|
21
|
+
'monthly',
|
|
22
|
+
'quarterly',
|
|
23
|
+
'yearly',
|
|
24
|
+
'lifetime',
|
|
25
|
+
];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Types
|
|
3
|
+
*
|
|
4
|
+
* Clean abstraction types for subscription data, independent of RevenueCat SDK.
|
|
5
|
+
*/
|
|
6
|
+
import type { SubscriptionPeriod } from './period';
|
|
7
|
+
/**
|
|
8
|
+
* Product pricing and billing information
|
|
9
|
+
*/
|
|
10
|
+
export interface SubscriptionProduct {
|
|
11
|
+
/** Product ID from the store */
|
|
12
|
+
productId: string;
|
|
13
|
+
/** Product display name */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Product description */
|
|
16
|
+
description?: string;
|
|
17
|
+
/** Numeric price value */
|
|
18
|
+
price: number;
|
|
19
|
+
/** Formatted price string (e.g., "$9.99") */
|
|
20
|
+
priceString: string;
|
|
21
|
+
/** Currency code (e.g., "USD") */
|
|
22
|
+
currency: string;
|
|
23
|
+
/** Parsed subscription period */
|
|
24
|
+
period: SubscriptionPeriod;
|
|
25
|
+
/** Raw ISO 8601 period duration */
|
|
26
|
+
periodDuration: string;
|
|
27
|
+
/** Trial period in ISO 8601 format (e.g., "P7D") */
|
|
28
|
+
trialPeriod?: string;
|
|
29
|
+
/** Introductory price string */
|
|
30
|
+
introPrice?: string;
|
|
31
|
+
/** Introductory price period in ISO 8601 format */
|
|
32
|
+
introPricePeriod?: string;
|
|
33
|
+
/** Number of intro price billing cycles */
|
|
34
|
+
introPriceCycles?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Package with optional product (undefined for free tier)
|
|
38
|
+
*/
|
|
39
|
+
export interface SubscriptionPackage {
|
|
40
|
+
/** Package identifier (e.g., "pro_monthly", "free") */
|
|
41
|
+
packageId: string;
|
|
42
|
+
/** Package display name */
|
|
43
|
+
name: string;
|
|
44
|
+
/** Product information, undefined for free tier */
|
|
45
|
+
product?: SubscriptionProduct;
|
|
46
|
+
/** Entitlement identifiers granted by this package */
|
|
47
|
+
entitlements: string[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Offer containing packages
|
|
51
|
+
*/
|
|
52
|
+
export interface SubscriptionOffer {
|
|
53
|
+
/** Offer identifier */
|
|
54
|
+
offerId: string;
|
|
55
|
+
/** Offer metadata from RevenueCat */
|
|
56
|
+
metadata?: Record<string, unknown>;
|
|
57
|
+
/** Packages in this offer */
|
|
58
|
+
packages: SubscriptionPackage[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Current subscription status
|
|
62
|
+
*/
|
|
63
|
+
export interface CurrentSubscription {
|
|
64
|
+
/** Whether user has an active subscription */
|
|
65
|
+
isActive: boolean;
|
|
66
|
+
/** Product identifier of the current subscription */
|
|
67
|
+
productId?: string;
|
|
68
|
+
/** Package identifier of the current subscription */
|
|
69
|
+
packageId?: string;
|
|
70
|
+
/** Active entitlement identifiers */
|
|
71
|
+
entitlements: string[];
|
|
72
|
+
/** Current subscription period */
|
|
73
|
+
period?: SubscriptionPeriod;
|
|
74
|
+
/** Subscription expiration date */
|
|
75
|
+
expirationDate?: Date;
|
|
76
|
+
/** Whether subscription will auto-renew */
|
|
77
|
+
willRenew?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Configuration for the free tier
|
|
81
|
+
*/
|
|
82
|
+
export interface FreeTierConfig {
|
|
83
|
+
/** Package ID for the free tier */
|
|
84
|
+
packageId: string;
|
|
85
|
+
/** Display name for the free tier */
|
|
86
|
+
name: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Package with calculated level for upgrade eligibility
|
|
90
|
+
*/
|
|
91
|
+
export interface PackageWithLevel extends SubscriptionPackage {
|
|
92
|
+
/** Calculated entitlement level (0 for free, higher = better) */
|
|
93
|
+
level: number;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=subscription.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../../src/types/subscription.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,0BAA0B;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,WAAW,EAAE,MAAM,CAAC;IACpB,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,mCAAmC;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2CAA2C;IAC3C,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,sDAAsD;IACtD,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,6BAA6B;IAC7B,QAAQ,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,8CAA8C;IAC9C,QAAQ,EAAE,OAAO,CAAC;IAClB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,kCAAkC;IAClC,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,mCAAmC;IACnC,cAAc,CAAC,EAAE,IAAI,CAAC;IACtB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,gBAAiB,SAAQ,mBAAmB;IAC3D,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;CACf"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility Exports
|
|
3
|
+
*/
|
|
4
|
+
export { parseISO8601Period, getPeriodRank, comparePeriods, isPeriodGreaterOrEqual, } from './period-parser';
|
|
5
|
+
export { calculatePackageLevels, addLevelsToPackages, getPackageLevel, findUpgradeablePackages, } from './level-calculator';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,EACf,uBAAuB,GACxB,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level Calculator Utilities
|
|
3
|
+
*
|
|
4
|
+
* Calculate entitlement levels based on price comparison within the same period.
|
|
5
|
+
*/
|
|
6
|
+
import type { PackageWithLevel, SubscriptionPackage } from '../types/subscription';
|
|
7
|
+
/**
|
|
8
|
+
* Calculate entitlement levels for packages
|
|
9
|
+
*
|
|
10
|
+
* Level is determined by comparing prices of packages with the same period.
|
|
11
|
+
* Higher price = higher level. Free tier always has level 0.
|
|
12
|
+
*
|
|
13
|
+
* @param packages List of packages to calculate levels for
|
|
14
|
+
* @returns Map of packageId to level
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Given packages:
|
|
18
|
+
* // - free (no product)
|
|
19
|
+
* // - basic_monthly ($5)
|
|
20
|
+
* // - pro_monthly ($10)
|
|
21
|
+
* // - basic_yearly ($50)
|
|
22
|
+
* // - pro_yearly ($100)
|
|
23
|
+
* //
|
|
24
|
+
* // Returns:
|
|
25
|
+
* // - free: 0
|
|
26
|
+
* // - basic_monthly: 1
|
|
27
|
+
* // - pro_monthly: 2
|
|
28
|
+
* // - basic_yearly: 1
|
|
29
|
+
* // - pro_yearly: 2
|
|
30
|
+
*/
|
|
31
|
+
export declare function calculatePackageLevels(packages: SubscriptionPackage[]): Map<string, number>;
|
|
32
|
+
/**
|
|
33
|
+
* Add level information to packages
|
|
34
|
+
*
|
|
35
|
+
* @param packages List of packages
|
|
36
|
+
* @returns Packages with level information
|
|
37
|
+
*/
|
|
38
|
+
export declare function addLevelsToPackages(packages: SubscriptionPackage[]): PackageWithLevel[];
|
|
39
|
+
/**
|
|
40
|
+
* Get the level of a specific package
|
|
41
|
+
*
|
|
42
|
+
* @param packageId Package ID to look up
|
|
43
|
+
* @param packages All packages for context
|
|
44
|
+
* @returns Level number (0 for free, higher = better)
|
|
45
|
+
*/
|
|
46
|
+
export declare function getPackageLevel(packageId: string, packages: SubscriptionPackage[]): number;
|
|
47
|
+
/**
|
|
48
|
+
* Parameters for findUpgradeablePackages
|
|
49
|
+
*/
|
|
50
|
+
export interface FindUpgradeableParams {
|
|
51
|
+
/** Current package ID */
|
|
52
|
+
packageId?: string | null;
|
|
53
|
+
/** Current product ID (fallback if packageId not found) */
|
|
54
|
+
productId?: string | null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Find packages that are upgrades from the current package
|
|
58
|
+
*
|
|
59
|
+
* A package is an upgrade if:
|
|
60
|
+
* - Its period is >= current period, AND
|
|
61
|
+
* - Its level is >= current level (but not the exact same package)
|
|
62
|
+
*
|
|
63
|
+
* @param current Current package/product identifiers (or null/undefined for no subscription)
|
|
64
|
+
* @param packages All available packages
|
|
65
|
+
* @returns List of package IDs that are valid upgrades
|
|
66
|
+
*/
|
|
67
|
+
export declare function findUpgradeablePackages(current: string | FindUpgradeableParams | null | undefined, packages: SubscriptionPackage[]): string[];
|
|
68
|
+
//# sourceMappingURL=level-calculator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"level-calculator.d.ts","sourceRoot":"","sources":["../../src/utils/level-calculator.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,mBAAmB,EACpB,MAAM,uBAAuB,CAAC;AAG/B;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,mBAAmB,EAAE,GAC9B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAiDrB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,mBAAmB,EAAE,GAC9B,gBAAgB,EAAE,CAOpB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,mBAAmB,EAAE,GAC9B,MAAM,CAGR;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI,GAAG,SAAS,EAC1D,QAAQ,EAAE,mBAAmB,EAAE,GAC9B,MAAM,EAAE,CA2EV"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level Calculator Utilities
|
|
3
|
+
*
|
|
4
|
+
* Calculate entitlement levels based on price comparison within the same period.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Calculate entitlement levels for packages
|
|
8
|
+
*
|
|
9
|
+
* Level is determined by comparing prices of packages with the same period.
|
|
10
|
+
* Higher price = higher level. Free tier always has level 0.
|
|
11
|
+
*
|
|
12
|
+
* @param packages List of packages to calculate levels for
|
|
13
|
+
* @returns Map of packageId to level
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // Given packages:
|
|
17
|
+
* // - free (no product)
|
|
18
|
+
* // - basic_monthly ($5)
|
|
19
|
+
* // - pro_monthly ($10)
|
|
20
|
+
* // - basic_yearly ($50)
|
|
21
|
+
* // - pro_yearly ($100)
|
|
22
|
+
* //
|
|
23
|
+
* // Returns:
|
|
24
|
+
* // - free: 0
|
|
25
|
+
* // - basic_monthly: 1
|
|
26
|
+
* // - pro_monthly: 2
|
|
27
|
+
* // - basic_yearly: 1
|
|
28
|
+
* // - pro_yearly: 2
|
|
29
|
+
*/
|
|
30
|
+
export function calculatePackageLevels(packages) {
|
|
31
|
+
const levels = new Map();
|
|
32
|
+
// Group packages by period
|
|
33
|
+
const byPeriod = new Map();
|
|
34
|
+
for (const pkg of packages) {
|
|
35
|
+
if (!pkg.product) {
|
|
36
|
+
// Free tier
|
|
37
|
+
levels.set(pkg.packageId, 0);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const period = pkg.product.period;
|
|
41
|
+
const existing = byPeriod.get(period) ?? [];
|
|
42
|
+
existing.push(pkg);
|
|
43
|
+
byPeriod.set(period, existing);
|
|
44
|
+
}
|
|
45
|
+
// For each period, sort by price and assign levels
|
|
46
|
+
for (const [_period, periodPackages] of byPeriod) {
|
|
47
|
+
// Sort by price ascending
|
|
48
|
+
const sorted = [...periodPackages].sort((a, b) => {
|
|
49
|
+
const priceA = a.product?.price ?? 0;
|
|
50
|
+
const priceB = b.product?.price ?? 0;
|
|
51
|
+
return priceA - priceB;
|
|
52
|
+
});
|
|
53
|
+
// Assign levels (1, 2, 3, ...)
|
|
54
|
+
// Same price = same level
|
|
55
|
+
let currentLevel = 0;
|
|
56
|
+
let lastPrice = null;
|
|
57
|
+
for (const pkg of sorted) {
|
|
58
|
+
const price = pkg.product?.price ?? 0;
|
|
59
|
+
if (lastPrice === null || price > lastPrice) {
|
|
60
|
+
currentLevel++;
|
|
61
|
+
lastPrice = price;
|
|
62
|
+
}
|
|
63
|
+
levels.set(pkg.packageId, currentLevel);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return levels;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Add level information to packages
|
|
70
|
+
*
|
|
71
|
+
* @param packages List of packages
|
|
72
|
+
* @returns Packages with level information
|
|
73
|
+
*/
|
|
74
|
+
export function addLevelsToPackages(packages) {
|
|
75
|
+
const levels = calculatePackageLevels(packages);
|
|
76
|
+
return packages.map(pkg => ({
|
|
77
|
+
...pkg,
|
|
78
|
+
level: levels.get(pkg.packageId) ?? 0,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get the level of a specific package
|
|
83
|
+
*
|
|
84
|
+
* @param packageId Package ID to look up
|
|
85
|
+
* @param packages All packages for context
|
|
86
|
+
* @returns Level number (0 for free, higher = better)
|
|
87
|
+
*/
|
|
88
|
+
export function getPackageLevel(packageId, packages) {
|
|
89
|
+
const levels = calculatePackageLevels(packages);
|
|
90
|
+
return levels.get(packageId) ?? 0;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Find packages that are upgrades from the current package
|
|
94
|
+
*
|
|
95
|
+
* A package is an upgrade if:
|
|
96
|
+
* - Its period is >= current period, AND
|
|
97
|
+
* - Its level is >= current level (but not the exact same package)
|
|
98
|
+
*
|
|
99
|
+
* @param current Current package/product identifiers (or null/undefined for no subscription)
|
|
100
|
+
* @param packages All available packages
|
|
101
|
+
* @returns List of package IDs that are valid upgrades
|
|
102
|
+
*/
|
|
103
|
+
export function findUpgradeablePackages(current, packages) {
|
|
104
|
+
// Normalize parameters
|
|
105
|
+
let currentPackageId;
|
|
106
|
+
let currentProductId;
|
|
107
|
+
if (typeof current === 'string') {
|
|
108
|
+
currentPackageId = current;
|
|
109
|
+
}
|
|
110
|
+
else if (current) {
|
|
111
|
+
currentPackageId = current.packageId;
|
|
112
|
+
currentProductId = current.productId;
|
|
113
|
+
}
|
|
114
|
+
// No current subscription = all packages are subscribable
|
|
115
|
+
if (!currentPackageId && !currentProductId) {
|
|
116
|
+
return packages.map(p => p.packageId);
|
|
117
|
+
}
|
|
118
|
+
// Try to find by packageId first, then by productId
|
|
119
|
+
let currentPkg = currentPackageId
|
|
120
|
+
? packages.find(p => p.packageId === currentPackageId)
|
|
121
|
+
: undefined;
|
|
122
|
+
if (!currentPkg && currentProductId) {
|
|
123
|
+
currentPkg = packages.find(p => p.product?.productId === currentProductId);
|
|
124
|
+
}
|
|
125
|
+
if (!currentPkg) {
|
|
126
|
+
// Current package not found, return all
|
|
127
|
+
return packages.map(p => p.packageId);
|
|
128
|
+
}
|
|
129
|
+
// Use the matched package's ID (important when matched by productId)
|
|
130
|
+
const matchedPackageId = currentPkg.packageId;
|
|
131
|
+
// Free tier current = all paid packages are upgrades
|
|
132
|
+
if (!currentPkg.product) {
|
|
133
|
+
return packages.filter(p => p.product !== undefined).map(p => p.packageId);
|
|
134
|
+
}
|
|
135
|
+
const levels = calculatePackageLevels(packages);
|
|
136
|
+
const currentLevel = levels.get(matchedPackageId) ?? 0;
|
|
137
|
+
const currentPeriod = currentPkg.product.period;
|
|
138
|
+
const upgrades = [];
|
|
139
|
+
for (const pkg of packages) {
|
|
140
|
+
// Skip free tier (can't upgrade to free)
|
|
141
|
+
if (!pkg.product)
|
|
142
|
+
continue;
|
|
143
|
+
// Skip current package (by packageId)
|
|
144
|
+
if (pkg.packageId === matchedPackageId) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const pkgLevel = levels.get(pkg.packageId) ?? 0;
|
|
148
|
+
const pkgPeriod = pkg.product.period;
|
|
149
|
+
// Must have >= period and >= level
|
|
150
|
+
const periodRanks = {
|
|
151
|
+
weekly: 1,
|
|
152
|
+
monthly: 2,
|
|
153
|
+
quarterly: 3,
|
|
154
|
+
yearly: 4,
|
|
155
|
+
lifetime: 5,
|
|
156
|
+
};
|
|
157
|
+
const isPeriodOk = periodRanks[pkgPeriod] >= periodRanks[currentPeriod];
|
|
158
|
+
const isLevelOk = pkgLevel >= currentLevel;
|
|
159
|
+
if (isPeriodOk && isLevelOk) {
|
|
160
|
+
upgrades.push(pkg.packageId);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return upgrades;
|
|
164
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Period Parser Utilities
|
|
3
|
+
*
|
|
4
|
+
* Parse ISO 8601 duration strings to standard subscription periods.
|
|
5
|
+
*/
|
|
6
|
+
import type { SubscriptionPeriod } from '../types/period';
|
|
7
|
+
/**
|
|
8
|
+
* Parse ISO 8601 duration string to SubscriptionPeriod
|
|
9
|
+
*
|
|
10
|
+
* @param duration ISO 8601 duration (e.g., "P1M", "P1Y", "P7D", "P1W")
|
|
11
|
+
* @returns Parsed SubscriptionPeriod
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* parseISO8601Period("P1M") // "monthly"
|
|
15
|
+
* parseISO8601Period("P1Y") // "yearly"
|
|
16
|
+
* parseISO8601Period("P7D") // "weekly"
|
|
17
|
+
* parseISO8601Period("P1W") // "weekly"
|
|
18
|
+
* parseISO8601Period("P3M") // "quarterly"
|
|
19
|
+
* parseISO8601Period(null) // "lifetime"
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseISO8601Period(duration: string | null | undefined): SubscriptionPeriod;
|
|
22
|
+
/**
|
|
23
|
+
* Get numeric rank for a period (higher = longer)
|
|
24
|
+
*
|
|
25
|
+
* @param period SubscriptionPeriod
|
|
26
|
+
* @returns Numeric rank (1-5)
|
|
27
|
+
*/
|
|
28
|
+
export declare function getPeriodRank(period: SubscriptionPeriod): number;
|
|
29
|
+
/**
|
|
30
|
+
* Compare two periods
|
|
31
|
+
*
|
|
32
|
+
* @param a First period
|
|
33
|
+
* @param b Second period
|
|
34
|
+
* @returns Negative if a < b, 0 if equal, positive if a > b
|
|
35
|
+
*/
|
|
36
|
+
export declare function comparePeriods(a: SubscriptionPeriod, b: SubscriptionPeriod): number;
|
|
37
|
+
/**
|
|
38
|
+
* Check if period A is greater than or equal to period B
|
|
39
|
+
*
|
|
40
|
+
* @param a Period to check
|
|
41
|
+
* @param b Period to compare against
|
|
42
|
+
* @returns true if a >= b
|
|
43
|
+
*/
|
|
44
|
+
export declare function isPeriodGreaterOrEqual(a: SubscriptionPeriod, b: SubscriptionPeriod): boolean;
|
|
45
|
+
//# sourceMappingURL=period-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"period-parser.d.ts","sourceRoot":"","sources":["../../src/utils/period-parser.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAG1D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAClC,kBAAkB,CA2DpB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,CAAC,EAAE,kBAAkB,EACrB,CAAC,EAAE,kBAAkB,GACpB,MAAM,CAER;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,CAAC,EAAE,kBAAkB,EACrB,CAAC,EAAE,kBAAkB,GACpB,OAAO,CAET"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Period Parser Utilities
|
|
3
|
+
*
|
|
4
|
+
* Parse ISO 8601 duration strings to standard subscription periods.
|
|
5
|
+
*/
|
|
6
|
+
import { PERIOD_RANKS } from '../types/period';
|
|
7
|
+
/**
|
|
8
|
+
* Parse ISO 8601 duration string to SubscriptionPeriod
|
|
9
|
+
*
|
|
10
|
+
* @param duration ISO 8601 duration (e.g., "P1M", "P1Y", "P7D", "P1W")
|
|
11
|
+
* @returns Parsed SubscriptionPeriod
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* parseISO8601Period("P1M") // "monthly"
|
|
15
|
+
* parseISO8601Period("P1Y") // "yearly"
|
|
16
|
+
* parseISO8601Period("P7D") // "weekly"
|
|
17
|
+
* parseISO8601Period("P1W") // "weekly"
|
|
18
|
+
* parseISO8601Period("P3M") // "quarterly"
|
|
19
|
+
* parseISO8601Period(null) // "lifetime"
|
|
20
|
+
*/
|
|
21
|
+
export function parseISO8601Period(duration) {
|
|
22
|
+
if (!duration) {
|
|
23
|
+
return 'lifetime';
|
|
24
|
+
}
|
|
25
|
+
const normalized = duration.toUpperCase();
|
|
26
|
+
// Weekly: P1W or P7D
|
|
27
|
+
if (normalized === 'P1W' || normalized === 'P7D') {
|
|
28
|
+
return 'weekly';
|
|
29
|
+
}
|
|
30
|
+
// Monthly: P1M
|
|
31
|
+
if (normalized === 'P1M') {
|
|
32
|
+
return 'monthly';
|
|
33
|
+
}
|
|
34
|
+
// Quarterly: P3M
|
|
35
|
+
if (normalized === 'P3M') {
|
|
36
|
+
return 'quarterly';
|
|
37
|
+
}
|
|
38
|
+
// Yearly: P1Y or P12M
|
|
39
|
+
if (normalized === 'P1Y' || normalized === 'P12M') {
|
|
40
|
+
return 'yearly';
|
|
41
|
+
}
|
|
42
|
+
// Try to parse more complex durations
|
|
43
|
+
const match = normalized.match(/^P(\d+)([DWMY])$/);
|
|
44
|
+
if (match) {
|
|
45
|
+
const value = parseInt(match[1], 10);
|
|
46
|
+
const unit = match[2];
|
|
47
|
+
switch (unit) {
|
|
48
|
+
case 'D':
|
|
49
|
+
if (value === 7)
|
|
50
|
+
return 'weekly';
|
|
51
|
+
if (value >= 28 && value <= 31)
|
|
52
|
+
return 'monthly';
|
|
53
|
+
if (value >= 84 && value <= 93)
|
|
54
|
+
return 'quarterly';
|
|
55
|
+
if (value >= 365)
|
|
56
|
+
return 'yearly';
|
|
57
|
+
break;
|
|
58
|
+
case 'W':
|
|
59
|
+
if (value === 1)
|
|
60
|
+
return 'weekly';
|
|
61
|
+
if (value === 4 || value === 5)
|
|
62
|
+
return 'monthly';
|
|
63
|
+
if (value === 13)
|
|
64
|
+
return 'quarterly';
|
|
65
|
+
if (value === 52)
|
|
66
|
+
return 'yearly';
|
|
67
|
+
break;
|
|
68
|
+
case 'M':
|
|
69
|
+
if (value === 1)
|
|
70
|
+
return 'monthly';
|
|
71
|
+
if (value === 3)
|
|
72
|
+
return 'quarterly';
|
|
73
|
+
if (value === 6)
|
|
74
|
+
return 'quarterly'; // Treat 6 months as quarterly for simplicity
|
|
75
|
+
if (value === 12)
|
|
76
|
+
return 'yearly';
|
|
77
|
+
break;
|
|
78
|
+
case 'Y':
|
|
79
|
+
return 'yearly';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Default to monthly for unrecognized patterns
|
|
83
|
+
return 'monthly';
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get numeric rank for a period (higher = longer)
|
|
87
|
+
*
|
|
88
|
+
* @param period SubscriptionPeriod
|
|
89
|
+
* @returns Numeric rank (1-5)
|
|
90
|
+
*/
|
|
91
|
+
export function getPeriodRank(period) {
|
|
92
|
+
return PERIOD_RANKS[period];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Compare two periods
|
|
96
|
+
*
|
|
97
|
+
* @param a First period
|
|
98
|
+
* @param b Second period
|
|
99
|
+
* @returns Negative if a < b, 0 if equal, positive if a > b
|
|
100
|
+
*/
|
|
101
|
+
export function comparePeriods(a, b) {
|
|
102
|
+
return getPeriodRank(a) - getPeriodRank(b);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if period A is greater than or equal to period B
|
|
106
|
+
*
|
|
107
|
+
* @param a Period to check
|
|
108
|
+
* @param b Period to compare against
|
|
109
|
+
* @returns true if a >= b
|
|
110
|
+
*/
|
|
111
|
+
export function isPeriodGreaterOrEqual(a, b) {
|
|
112
|
+
return getPeriodRank(a) >= getPeriodRank(b);
|
|
113
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sudobility/subscription_lib",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Cross-platform subscription management with RevenueCat adapter pattern",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"dev": "tsc --watch",
|
|
22
|
+
"clean": "rm -rf dist",
|
|
23
|
+
"typecheck": "bunx tsc --noEmit",
|
|
24
|
+
"lint": "bunx eslint src",
|
|
25
|
+
"lint:fix": "bunx eslint src --fix",
|
|
26
|
+
"format": "bunx prettier --write \"src/**/*.{ts,tsx,js,jsx,json}\"",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"test:watch": "vitest",
|
|
29
|
+
"prepublishOnly": "bun run build"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": "^19.0.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"vitest": "^4.0.4",
|
|
36
|
+
"@eslint/js": "^9.0.0",
|
|
37
|
+
"@types/bun": "^1.2.8",
|
|
38
|
+
"@types/react": "^19.2.5",
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
40
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
41
|
+
"eslint": "^9.0.0",
|
|
42
|
+
"eslint-config-prettier": "^10.0.0",
|
|
43
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
44
|
+
"prettier": "^3.0.0",
|
|
45
|
+
"typescript": "~5.9.3"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "https://github.com/sudobility/subscription_lib.git"
|
|
53
|
+
},
|
|
54
|
+
"license": "MIT"
|
|
55
|
+
}
|