@umituz/react-native-subscription 2.10.9 → 2.10.11
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/package.json +1 -1
- package/src/presentation/components/paywall/SubscriptionPackageList.tsx +26 -9
- package/src/presentation/components/paywall/SubscriptionPlanCard.tsx +2 -14
- package/src/revenuecat/infrastructure/managers/SubscriptionManager.ts +1 -5
- package/src/revenuecat/infrastructure/services/RevenueCatInitializer.ts +52 -68
- package/src/utils/packagePeriodUtils.ts +51 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.11",
|
|
4
4
|
"description": "Complete subscription management with RevenueCat, paywall UI, and credits system for React Native apps",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -4,6 +4,7 @@ import { AtomicText } from "@umituz/react-native-design-system";
|
|
|
4
4
|
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
5
5
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
6
6
|
import { SubscriptionPlanCard } from "./SubscriptionPlanCard";
|
|
7
|
+
import { isYearlyPackage } from "../../../utils/packagePeriodUtils";
|
|
7
8
|
|
|
8
9
|
interface SubscriptionPackageListProps {
|
|
9
10
|
isLoading: boolean;
|
|
@@ -12,6 +13,8 @@ interface SubscriptionPackageListProps {
|
|
|
12
13
|
loadingText: string;
|
|
13
14
|
emptyText: string;
|
|
14
15
|
onSelect: (pkg: PurchasesPackage) => void;
|
|
16
|
+
/** Optional: Manually specify which package should show "Best Value" badge by identifier */
|
|
17
|
+
bestValueIdentifier?: string;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
export const SubscriptionPackageList: React.FC<SubscriptionPackageListProps> = React.memo(
|
|
@@ -22,6 +25,7 @@ export const SubscriptionPackageList: React.FC<SubscriptionPackageListProps> = R
|
|
|
22
25
|
loadingText,
|
|
23
26
|
emptyText,
|
|
24
27
|
onSelect,
|
|
28
|
+
bestValueIdentifier,
|
|
25
29
|
}) => {
|
|
26
30
|
const tokens = useAppDesignTokens();
|
|
27
31
|
const hasPackages = packages.length > 0;
|
|
@@ -69,15 +73,28 @@ export const SubscriptionPackageList: React.FC<SubscriptionPackageListProps> = R
|
|
|
69
73
|
|
|
70
74
|
return (
|
|
71
75
|
<View style={styles.packagesContainer}>
|
|
72
|
-
{packages.map((pkg
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
isBestValue=
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
{packages.map((pkg) => {
|
|
77
|
+
// Determine if this package should show "Best Value" badge
|
|
78
|
+
let isBestValue = false;
|
|
79
|
+
|
|
80
|
+
if (bestValueIdentifier) {
|
|
81
|
+
// Use manual override if provided
|
|
82
|
+
isBestValue = pkg.product.identifier === bestValueIdentifier;
|
|
83
|
+
} else {
|
|
84
|
+
// Auto-detect: mark yearly packages as best value
|
|
85
|
+
isBestValue = isYearlyPackage(pkg);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<SubscriptionPlanCard
|
|
90
|
+
key={pkg.product.identifier}
|
|
91
|
+
package={pkg}
|
|
92
|
+
isSelected={selectedPkg?.product.identifier === pkg.product.identifier}
|
|
93
|
+
onSelect={() => onSelect(pkg)}
|
|
94
|
+
isBestValue={isBestValue}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
81
98
|
</View>
|
|
82
99
|
);
|
|
83
100
|
}
|
|
@@ -12,6 +12,7 @@ import { formatPrice } from "../../../utils/priceUtils";
|
|
|
12
12
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
13
13
|
import { BestValueBadge } from "./BestValueBadge";
|
|
14
14
|
|
|
15
|
+
import { getPeriodLabel, isYearlyPackage } from "../../../utils/packagePeriodUtils";
|
|
15
16
|
// @ts-ignore
|
|
16
17
|
import { LinearGradient } from "expo-linear-gradient";
|
|
17
18
|
|
|
@@ -22,26 +23,13 @@ interface SubscriptionPlanCardProps {
|
|
|
22
23
|
isBestValue?: boolean;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
const getPeriodLabel = (period: string | null | undefined): string => {
|
|
26
|
-
if (!period) return "";
|
|
27
|
-
if (period.includes("Y") || period.includes("year")) return "yearly";
|
|
28
|
-
if (period.includes("M") || period.includes("month")) return "monthly";
|
|
29
|
-
if (period.includes("W") || period.includes("week")) return "weekly";
|
|
30
|
-
if (period.includes("D") || period.includes("day")) return "daily";
|
|
31
|
-
return "";
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const isYearlyPeriod = (period: string | null | undefined): boolean => {
|
|
35
|
-
return period?.includes("Y") || period?.includes("year") || false;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
26
|
export const SubscriptionPlanCard: React.FC<SubscriptionPlanCardProps> =
|
|
39
27
|
React.memo(({ package: pkg, isSelected, onSelect, isBestValue = false }) => {
|
|
40
28
|
const tokens = useAppDesignTokens();
|
|
41
29
|
const { t } = useLocalization();
|
|
42
30
|
|
|
43
31
|
const period = pkg.product.subscriptionPeriod;
|
|
44
|
-
const isYearly =
|
|
32
|
+
const isYearly = isYearlyPackage(pkg);
|
|
45
33
|
const periodLabel = getPeriodLabel(period);
|
|
46
34
|
const price = formatPrice(pkg.product.price, pkg.product.currencyCode);
|
|
47
35
|
const monthlyEquivalent = isYearly
|
|
@@ -91,10 +91,6 @@ class SubscriptionManagerImpl {
|
|
|
91
91
|
if (existingPromise) return existingPromise;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
if (this.initCache.shouldReinitialize(effectiveUserId) && this.serviceInstance) {
|
|
95
|
-
await this.serviceInstance.reset();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
94
|
const promise = this.performInitialization(effectiveUserId);
|
|
99
95
|
this.initCache.setPromise(promise, effectiveUserId);
|
|
100
96
|
|
|
@@ -107,7 +103,7 @@ class SubscriptionManagerImpl {
|
|
|
107
103
|
|
|
108
104
|
isInitializedForUser(userId: string): boolean {
|
|
109
105
|
return this.serviceInstance?.isInitialized() === true &&
|
|
110
|
-
|
|
106
|
+
this.initCache.getCurrentUserId() === userId;
|
|
111
107
|
}
|
|
112
108
|
|
|
113
109
|
async getPackages(): Promise<PurchasesPackage[]> {
|
|
@@ -11,7 +11,6 @@ import { resolveApiKey } from "../utils/ApiKeyResolver";
|
|
|
11
11
|
import {
|
|
12
12
|
trackPackageError,
|
|
13
13
|
addPackageBreadcrumb,
|
|
14
|
-
trackPackageWarning,
|
|
15
14
|
} from "@umituz/react-native-sentry";
|
|
16
15
|
|
|
17
16
|
export interface InitializerDeps {
|
|
@@ -23,6 +22,9 @@ export interface InitializerDeps {
|
|
|
23
22
|
setCurrentUserId: (userId: string) => void;
|
|
24
23
|
}
|
|
25
24
|
|
|
25
|
+
// Track if Purchases.configure has been called globally
|
|
26
|
+
let isPurchasesConfigured = false;
|
|
27
|
+
|
|
26
28
|
export async function initializeSDK(
|
|
27
29
|
deps: InitializerDeps,
|
|
28
30
|
userId: string,
|
|
@@ -31,71 +33,66 @@ export async function initializeSDK(
|
|
|
31
33
|
addPackageBreadcrumb("subscription", "SDK initialization started", {
|
|
32
34
|
userId,
|
|
33
35
|
hasApiKey: !!apiKey,
|
|
36
|
+
isAlreadyConfigured: isPurchasesConfigured,
|
|
34
37
|
});
|
|
35
38
|
|
|
36
39
|
if (__DEV__) {
|
|
37
|
-
console.log("[RevenueCat] initializeSDK() called with userId:", userId);
|
|
40
|
+
console.log("[RevenueCat] initializeSDK() called with userId:", userId, "isPurchasesConfigured:", isPurchasesConfigured);
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
//
|
|
41
|
-
if (deps.isInitialized()) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
userId,
|
|
46
|
-
});
|
|
43
|
+
// Case 1: Already initialized with the same user ID
|
|
44
|
+
if (deps.isInitialized() && deps.getCurrentUserId() === userId) {
|
|
45
|
+
if (__DEV__) {
|
|
46
|
+
console.log("[RevenueCat] Already initialized with same userId, skipping configure");
|
|
47
|
+
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{
|
|
64
|
-
packageName: "subscription",
|
|
65
|
-
operation: "get_current_state",
|
|
66
|
-
userId,
|
|
67
|
-
}
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
if (__DEV__) {
|
|
71
|
-
console.log("[RevenueCat] Failed to get current state:", error);
|
|
49
|
+
try {
|
|
50
|
+
const [customerInfo, offerings] = await Promise.all([
|
|
51
|
+
Purchases.getCustomerInfo(),
|
|
52
|
+
Purchases.getOfferings(),
|
|
53
|
+
]);
|
|
54
|
+
const entitlementId = deps.config.entitlementIdentifier;
|
|
55
|
+
const hasPremium = !!customerInfo.entitlements.active[entitlementId];
|
|
56
|
+
return { success: true, offering: offerings.current, hasPremium };
|
|
57
|
+
} catch (error) {
|
|
58
|
+
trackPackageError(
|
|
59
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
60
|
+
{
|
|
61
|
+
packageName: "subscription",
|
|
62
|
+
operation: "get_current_state",
|
|
63
|
+
userId,
|
|
72
64
|
}
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
addPackageBreadcrumb("subscription", "User changed, logging out previous user", {
|
|
77
|
-
previousUserId: currentUserId,
|
|
78
|
-
newUserId: userId,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
if (__DEV__) {
|
|
82
|
-
console.log("[RevenueCat] Different userId, will re-configure");
|
|
83
|
-
}
|
|
84
|
-
// Different userId - need to logout first
|
|
85
|
-
try {
|
|
86
|
-
await Purchases.logOut();
|
|
87
|
-
} catch (error) {
|
|
88
|
-
trackPackageWarning("subscription", "Logout failed during user change", {
|
|
89
|
-
error: error instanceof Error ? error.message : String(error),
|
|
90
|
-
previousUserId: currentUserId,
|
|
91
|
-
newUserId: userId,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
65
|
+
);
|
|
66
|
+
return { success: false, offering: null, hasPremium: false };
|
|
94
67
|
}
|
|
95
68
|
}
|
|
96
69
|
|
|
70
|
+
// Case 2: Already configured but different user or re-initializing
|
|
71
|
+
if (isPurchasesConfigured) {
|
|
72
|
+
if (__DEV__) {
|
|
73
|
+
console.log("[RevenueCat] SDK already configured, using logIn for userId:", userId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const { customerInfo } = await Purchases.logIn(userId);
|
|
78
|
+
|
|
79
|
+
deps.setInitialized(true);
|
|
80
|
+
deps.setCurrentUserId(userId);
|
|
97
81
|
|
|
82
|
+
const offerings = await Purchases.getOfferings();
|
|
83
|
+
const entitlementId = deps.config.entitlementIdentifier;
|
|
84
|
+
const hasPremium = !!customerInfo.entitlements.active[entitlementId];
|
|
85
|
+
|
|
86
|
+
return { success: true, offering: offerings.current, hasPremium };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (__DEV__) console.warn("[RevenueCat] logIn failed:", error);
|
|
89
|
+
// If logIn fails, we don't necessarily want to re-configure if it's already configured
|
|
90
|
+
// But we can return failure
|
|
91
|
+
return { success: false, offering: null, hasPremium: false };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
98
94
|
|
|
95
|
+
// Case 3: First time configuration
|
|
99
96
|
const key = apiKey || resolveApiKey(deps.config);
|
|
100
97
|
if (!key) {
|
|
101
98
|
const error = new Error("No RevenueCat API key available");
|
|
@@ -104,38 +101,25 @@ export async function initializeSDK(
|
|
|
104
101
|
operation: "sdk_init_no_key",
|
|
105
102
|
userId,
|
|
106
103
|
});
|
|
107
|
-
|
|
108
|
-
if (__DEV__) {
|
|
109
|
-
console.log("[RevenueCat] No API key available");
|
|
110
|
-
}
|
|
111
104
|
return { success: false, offering: null, hasPremium: false };
|
|
112
105
|
}
|
|
113
106
|
|
|
114
107
|
try {
|
|
115
108
|
if (deps.isUsingTestStore()) {
|
|
116
|
-
addPackageBreadcrumb("subscription", "Using test store configuration", {
|
|
117
|
-
userId,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
109
|
if (__DEV__) {
|
|
121
110
|
console.log("[RevenueCat] Using Test Store key");
|
|
122
111
|
}
|
|
123
112
|
}
|
|
124
113
|
|
|
125
|
-
addPackageBreadcrumb("subscription", "Configuring SDK", { userId });
|
|
126
|
-
|
|
127
114
|
if (__DEV__) {
|
|
128
115
|
console.log("[RevenueCat] Calling Purchases.configure()...");
|
|
129
116
|
}
|
|
130
117
|
|
|
131
118
|
await Purchases.configure({ apiKey: key, appUserID: userId });
|
|
119
|
+
isPurchasesConfigured = true;
|
|
132
120
|
deps.setInitialized(true);
|
|
133
121
|
deps.setCurrentUserId(userId);
|
|
134
122
|
|
|
135
|
-
addPackageBreadcrumb("subscription", "SDK configured successfully", {
|
|
136
|
-
userId,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
123
|
if (__DEV__) {
|
|
140
124
|
console.log("[RevenueCat] SDK configured successfully");
|
|
141
125
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Period Utilities
|
|
3
|
+
* Helper functions for working with subscription periods
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get period label from subscription period string
|
|
10
|
+
*/
|
|
11
|
+
export const getPeriodLabel = (period: string | null | undefined): string => {
|
|
12
|
+
if (!period) return "";
|
|
13
|
+
if (period.includes("Y") || period.includes("year")) return "yearly";
|
|
14
|
+
if (period.includes("M") || period.includes("month")) return "monthly";
|
|
15
|
+
if (period.includes("W") || period.includes("week")) return "weekly";
|
|
16
|
+
if (period.includes("D") || period.includes("day")) return "daily";
|
|
17
|
+
return "";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if a package has a yearly subscription period
|
|
22
|
+
*/
|
|
23
|
+
export const isYearlyPackage = (pkg: PurchasesPackage): boolean => {
|
|
24
|
+
const period = pkg.product.subscriptionPeriod;
|
|
25
|
+
return period?.includes("Y") || period?.includes("year") || false;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a package has a monthly subscription period
|
|
30
|
+
*/
|
|
31
|
+
export const isMonthlyPackage = (pkg: PurchasesPackage): boolean => {
|
|
32
|
+
const period = pkg.product.subscriptionPeriod;
|
|
33
|
+
return period?.includes("M") || period?.includes("month") || false;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if a package has a weekly subscription period
|
|
38
|
+
*/
|
|
39
|
+
export const isWeeklyPackage = (pkg: PurchasesPackage): boolean => {
|
|
40
|
+
const period = pkg.product.subscriptionPeriod;
|
|
41
|
+
return period?.includes("W") || period?.includes("week") || false;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Find the first yearly package in an array of packages
|
|
46
|
+
*/
|
|
47
|
+
export const findYearlyPackage = (
|
|
48
|
+
packages: PurchasesPackage[]
|
|
49
|
+
): PurchasesPackage | undefined => {
|
|
50
|
+
return packages.find(isYearlyPackage);
|
|
51
|
+
};
|