@umituz/react-native-subscription 2.10.16 → 2.11.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/package.json +8 -8
- package/src/index.ts +3 -0
- package/src/infrastructure/repositories/CreditsRepository.ts +1 -1
- package/src/presentation/components/paywall/PaywallHeader.tsx +2 -13
- package/src/presentation/components/paywall/PaywallModal.tsx +2 -6
- package/src/presentation/components/paywall/SubscriptionModal.tsx +2 -8
- package/src/presentation/components/paywall/SubscriptionModalHeader.tsx +0 -2
- package/src/revenuecat/presentation/hooks/useCustomerInfo.ts +129 -0
- package/src/revenuecat/presentation/hooks/useSubscriptionQueries.ts +5 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.1",
|
|
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",
|
|
@@ -42,14 +42,14 @@
|
|
|
42
42
|
"react": ">=18.2.0",
|
|
43
43
|
"react-native": ">=0.74.0",
|
|
44
44
|
"react-native-purchases": ">=7.0.0",
|
|
45
|
-
"react-native-safe-area-context": ">=
|
|
45
|
+
"react-native-safe-area-context": ">=5.0.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@umituz/react-native-design-system": "
|
|
49
|
-
"@umituz/react-native-firebase": "
|
|
50
|
-
"@umituz/react-native-legal": "
|
|
51
|
-
"@umituz/react-native-localization": "
|
|
52
|
-
"@umituz/react-native-sentry": "
|
|
48
|
+
"@umituz/react-native-design-system": "^2.1.3",
|
|
49
|
+
"@umituz/react-native-firebase": "*",
|
|
50
|
+
"@umituz/react-native-legal": "*",
|
|
51
|
+
"@umituz/react-native-localization": "*",
|
|
52
|
+
"@umituz/react-native-sentry": "*",
|
|
53
53
|
"@tanstack/react-query": "^5.0.0",
|
|
54
54
|
"expo-constants": "~16.0.0",
|
|
55
55
|
"expo-linear-gradient": "~15.0.0",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@types/react": "~19.1.10",
|
|
59
59
|
"react": "19.1.0",
|
|
60
60
|
"react-native": "0.81.5",
|
|
61
|
-
"react-native-safe-area-context": "^
|
|
61
|
+
"react-native-safe-area-context": "^5.0.0",
|
|
62
62
|
"typescript": "~5.9.2"
|
|
63
63
|
},
|
|
64
64
|
"publishConfig": {
|
package/src/index.ts
CHANGED
|
@@ -334,6 +334,9 @@ export {
|
|
|
334
334
|
export { useRevenueCat } from "./revenuecat/presentation/hooks/useRevenueCat";
|
|
335
335
|
export type { UseRevenueCatResult } from "./revenuecat/presentation/hooks/useRevenueCat";
|
|
336
336
|
|
|
337
|
+
export { useCustomerInfo } from "./revenuecat/presentation/hooks/useCustomerInfo";
|
|
338
|
+
export type { UseCustomerInfoResult } from "./revenuecat/presentation/hooks/useCustomerInfo";
|
|
339
|
+
|
|
337
340
|
export {
|
|
338
341
|
useInitializeSubscription,
|
|
339
342
|
useSubscriptionPackages,
|
|
@@ -12,17 +12,14 @@ interface PaywallHeaderProps {
|
|
|
12
12
|
title: string;
|
|
13
13
|
subtitle?: string;
|
|
14
14
|
onClose: () => void;
|
|
15
|
-
variant?: "bottom-sheet" | "fullscreen" | "dialog";
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
export const PaywallHeader: React.FC<PaywallHeaderProps> = React.memo(
|
|
19
|
-
({ title, subtitle, onClose
|
|
18
|
+
({ title, subtitle, onClose }) => {
|
|
20
19
|
const tokens = useAppDesignTokens();
|
|
21
20
|
|
|
22
|
-
const containerStyle = variant === "fullscreen" ? styles.containerFullscreen : styles.container;
|
|
23
|
-
|
|
24
21
|
return (
|
|
25
|
-
<View style={
|
|
22
|
+
<View style={styles.container}>
|
|
26
23
|
<View style={styles.titleContainer}>
|
|
27
24
|
<AtomicText
|
|
28
25
|
type="headlineLarge"
|
|
@@ -58,14 +55,6 @@ PaywallHeader.displayName = "PaywallHeader";
|
|
|
58
55
|
|
|
59
56
|
const styles = StyleSheet.create({
|
|
60
57
|
container: {
|
|
61
|
-
flexDirection: "row",
|
|
62
|
-
justifyContent: "space-between",
|
|
63
|
-
alignItems: "flex-start",
|
|
64
|
-
paddingHorizontal: 24,
|
|
65
|
-
paddingTop: 8,
|
|
66
|
-
paddingBottom: 16,
|
|
67
|
-
},
|
|
68
|
-
containerFullscreen: {
|
|
69
58
|
flexDirection: "row",
|
|
70
59
|
justifyContent: "space-between",
|
|
71
60
|
alignItems: "flex-start",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import React, { useEffect } from "react";
|
|
7
7
|
import { View, StyleSheet } from "react-native";
|
|
8
|
-
import { BaseModal
|
|
8
|
+
import { BaseModal } from "@umituz/react-native-design-system";
|
|
9
9
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
10
10
|
import { usePaywall } from "../../hooks/usePaywall";
|
|
11
11
|
import { PaywallHeader } from "./PaywallHeader";
|
|
@@ -80,8 +80,6 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
80
80
|
processingText,
|
|
81
81
|
} = props;
|
|
82
82
|
|
|
83
|
-
const { modalLayout } = useResponsive();
|
|
84
|
-
|
|
85
83
|
const {
|
|
86
84
|
activeTab,
|
|
87
85
|
selectedCreditsPackageId,
|
|
@@ -102,12 +100,11 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
102
100
|
console.log("[PaywallModal] State:", {
|
|
103
101
|
visible,
|
|
104
102
|
activeTab,
|
|
105
|
-
modalLayout,
|
|
106
103
|
creditsPackagesCount: creditsPackages?.length ?? 0,
|
|
107
104
|
subscriptionPackagesCount: subscriptionPackages?.length ?? 0,
|
|
108
105
|
});
|
|
109
106
|
}
|
|
110
|
-
}, [visible, activeTab,
|
|
107
|
+
}, [visible, activeTab, creditsPackages?.length, subscriptionPackages?.length]);
|
|
111
108
|
|
|
112
109
|
return (
|
|
113
110
|
<BaseModal visible={visible} onClose={onClose}>
|
|
@@ -116,7 +113,6 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
116
113
|
title={title}
|
|
117
114
|
subtitle={subtitle}
|
|
118
115
|
onClose={onClose}
|
|
119
|
-
variant="fullscreen"
|
|
120
116
|
/>
|
|
121
117
|
|
|
122
118
|
<PaywallTabBar
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import React from "react";
|
|
7
7
|
import { View, StyleSheet, ScrollView } from "react-native";
|
|
8
|
-
import { useAppDesignTokens, BaseModal
|
|
8
|
+
import { useAppDesignTokens, BaseModal } from "@umituz/react-native-design-system";
|
|
9
9
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
10
10
|
|
|
11
11
|
import { SubscriptionModalHeader } from "./SubscriptionModalHeader";
|
|
@@ -66,7 +66,6 @@ export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo((p
|
|
|
66
66
|
} = props;
|
|
67
67
|
|
|
68
68
|
const tokens = useAppDesignTokens();
|
|
69
|
-
const { modalLayout } = useResponsive();
|
|
70
69
|
|
|
71
70
|
const {
|
|
72
71
|
selectedPkg,
|
|
@@ -85,7 +84,6 @@ export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo((p
|
|
|
85
84
|
visible,
|
|
86
85
|
isLoading,
|
|
87
86
|
packagesCount: packages?.length ?? 0,
|
|
88
|
-
modalLayout,
|
|
89
87
|
selectedPkg: selectedPkg?.identifier ?? null,
|
|
90
88
|
isProcessing,
|
|
91
89
|
});
|
|
@@ -98,15 +96,11 @@ export const SubscriptionModal: React.FC<SubscriptionModalProps> = React.memo((p
|
|
|
98
96
|
title={title}
|
|
99
97
|
subtitle={subtitle}
|
|
100
98
|
onClose={onClose}
|
|
101
|
-
variant="fullscreen"
|
|
102
99
|
/>
|
|
103
100
|
|
|
104
101
|
<ScrollView
|
|
105
102
|
style={styles.scrollView}
|
|
106
|
-
contentContainerStyle={
|
|
107
|
-
styles.scrollContent,
|
|
108
|
-
{ paddingHorizontal: modalLayout.horizontalPadding }
|
|
109
|
-
]}
|
|
103
|
+
contentContainerStyle={styles.scrollContent}
|
|
110
104
|
showsVerticalScrollIndicator={false}
|
|
111
105
|
bounces={false}
|
|
112
106
|
>
|
|
@@ -11,14 +11,12 @@ interface SubscriptionModalHeaderProps {
|
|
|
11
11
|
title: string;
|
|
12
12
|
subtitle?: string;
|
|
13
13
|
onClose: () => void;
|
|
14
|
-
variant?: "bottom-sheet" | "fullscreen" | "dialog";
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
export const SubscriptionModalHeader: React.FC<SubscriptionModalHeaderProps> = ({
|
|
18
17
|
title,
|
|
19
18
|
subtitle,
|
|
20
19
|
onClose,
|
|
21
|
-
variant = "bottom-sheet",
|
|
22
20
|
}) => {
|
|
23
21
|
const tokens = useAppDesignTokens();
|
|
24
22
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCustomerInfo Hook
|
|
3
|
+
* Fetches and manages RevenueCat CustomerInfo with real-time updates
|
|
4
|
+
*
|
|
5
|
+
* BEST PRACTICE: Always get expiration date from CustomerInfo (source of truth)
|
|
6
|
+
* Never calculate expiration dates client-side (purchaseDate + 1 year is WRONG)
|
|
7
|
+
*
|
|
8
|
+
* This hook provides:
|
|
9
|
+
* - Initial fetch from SDK cache (instant, no network)
|
|
10
|
+
* - Real-time listener for updates (renewals, purchases, restore)
|
|
11
|
+
* - Automatic cleanup on unmount
|
|
12
|
+
* - SDK caches CustomerInfo and fetches every ~5 minutes
|
|
13
|
+
*
|
|
14
|
+
* @see https://www.revenuecat.com/docs/customers/customer-info
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useEffect, useState, useCallback } from "react";
|
|
18
|
+
import Purchases, { type CustomerInfo } from "react-native-purchases";
|
|
19
|
+
import { addPackageBreadcrumb, trackPackageError } from "@umituz/react-native-sentry";
|
|
20
|
+
|
|
21
|
+
export interface UseCustomerInfoResult {
|
|
22
|
+
/** Current CustomerInfo from RevenueCat SDK */
|
|
23
|
+
customerInfo: CustomerInfo | null;
|
|
24
|
+
/** Loading state (only true on initial fetch) */
|
|
25
|
+
loading: boolean;
|
|
26
|
+
/** Error message if fetch failed */
|
|
27
|
+
error: string | null;
|
|
28
|
+
/** Manually refetch CustomerInfo (usually not needed, listener handles updates) */
|
|
29
|
+
refetch: () => Promise<void>;
|
|
30
|
+
/** Whether SDK is currently fetching */
|
|
31
|
+
isFetching: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Hook to get CustomerInfo from RevenueCat SDK
|
|
36
|
+
*
|
|
37
|
+
* Features:
|
|
38
|
+
* - SDK cache: First call returns cached data (instant)
|
|
39
|
+
* - Auto-updates: Listener triggers on renewals, purchases, restore
|
|
40
|
+
* - Network fetch: SDK fetches every ~5 minutes in background
|
|
41
|
+
* - Grace periods: Expiration dates include grace period automatically
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const { customerInfo, loading } = useCustomerInfo();
|
|
46
|
+
*
|
|
47
|
+
* // Check premium status
|
|
48
|
+
* const isPremium = !!customerInfo?.entitlements.active['premium'];
|
|
49
|
+
*
|
|
50
|
+
* // Get expiration date (ALWAYS from CustomerInfo, NEVER calculate!)
|
|
51
|
+
* const expiresAt = customerInfo?.entitlements.active['premium']?.expirationDate;
|
|
52
|
+
*
|
|
53
|
+
* // Check will renew
|
|
54
|
+
* const willRenew = customerInfo?.entitlements.active['premium']?.willRenew;
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @returns CustomerInfo and loading state
|
|
58
|
+
*/
|
|
59
|
+
export function useCustomerInfo(): UseCustomerInfoResult {
|
|
60
|
+
const [customerInfo, setCustomerInfo] = useState<CustomerInfo | null>(null);
|
|
61
|
+
const [loading, setLoading] = useState(true);
|
|
62
|
+
const [isFetching, setIsFetching] = useState(false);
|
|
63
|
+
const [error, setError] = useState<string | null>(null);
|
|
64
|
+
|
|
65
|
+
const fetchCustomerInfo = useCallback(async () => {
|
|
66
|
+
try {
|
|
67
|
+
setIsFetching(true);
|
|
68
|
+
setError(null);
|
|
69
|
+
|
|
70
|
+
addPackageBreadcrumb("subscription", "Fetching CustomerInfo", {});
|
|
71
|
+
|
|
72
|
+
// SDK returns cached data instantly if available
|
|
73
|
+
// Network fetch happens in background automatically
|
|
74
|
+
const info = await Purchases.getCustomerInfo();
|
|
75
|
+
|
|
76
|
+
setCustomerInfo(info);
|
|
77
|
+
|
|
78
|
+
addPackageBreadcrumb("subscription", "CustomerInfo fetched", {
|
|
79
|
+
hasActiveEntitlements: Object.keys(info.entitlements.active).length > 0,
|
|
80
|
+
latestExpiration: info.latestExpirationDate || "none",
|
|
81
|
+
});
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const errorMessage =
|
|
84
|
+
err instanceof Error ? err.message : "Failed to fetch customer info";
|
|
85
|
+
setError(errorMessage);
|
|
86
|
+
|
|
87
|
+
trackPackageError(
|
|
88
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
89
|
+
{
|
|
90
|
+
packageName: "subscription",
|
|
91
|
+
operation: "fetch_customer_info",
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
} finally {
|
|
95
|
+
setLoading(false);
|
|
96
|
+
setIsFetching(false);
|
|
97
|
+
}
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
// Initial fetch
|
|
102
|
+
fetchCustomerInfo();
|
|
103
|
+
|
|
104
|
+
// Listen for real-time updates (renewals, purchases, restore)
|
|
105
|
+
const listener = (info: CustomerInfo) => {
|
|
106
|
+
addPackageBreadcrumb("subscription", "CustomerInfo updated via listener", {
|
|
107
|
+
hasActiveEntitlements: Object.keys(info.entitlements.active).length > 0,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
setCustomerInfo(info);
|
|
111
|
+
setError(null);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
Purchases.addCustomerInfoUpdateListener(listener);
|
|
115
|
+
|
|
116
|
+
// Cleanup listener on unmount
|
|
117
|
+
return () => {
|
|
118
|
+
Purchases.removeCustomerInfoUpdateListener(listener);
|
|
119
|
+
};
|
|
120
|
+
}, [fetchCustomerInfo]);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
customerInfo,
|
|
124
|
+
loading,
|
|
125
|
+
error,
|
|
126
|
+
refetch: fetchCustomerInfo,
|
|
127
|
+
isFetching,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -10,7 +10,6 @@ import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionM
|
|
|
10
10
|
import {
|
|
11
11
|
trackPackageError,
|
|
12
12
|
addPackageBreadcrumb,
|
|
13
|
-
trackPackageEvent,
|
|
14
13
|
} from "@umituz/react-native-sentry";
|
|
15
14
|
|
|
16
15
|
/**
|
|
@@ -112,7 +111,7 @@ export const usePurchasePackage = (userId: string | undefined) => {
|
|
|
112
111
|
throw new Error("User not authenticated");
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
|
|
114
|
+
addPackageBreadcrumb("subscription", "Purchase started", {
|
|
116
115
|
packageId: pkg.identifier,
|
|
117
116
|
userId,
|
|
118
117
|
});
|
|
@@ -125,7 +124,7 @@ export const usePurchasePackage = (userId: string | undefined) => {
|
|
|
125
124
|
const success = await SubscriptionManager.purchasePackage(pkg);
|
|
126
125
|
|
|
127
126
|
if (success) {
|
|
128
|
-
|
|
127
|
+
addPackageBreadcrumb("subscription", "Purchase success", {
|
|
129
128
|
packageId: pkg.identifier,
|
|
130
129
|
userId,
|
|
131
130
|
});
|
|
@@ -135,7 +134,7 @@ export const usePurchasePackage = (userId: string | undefined) => {
|
|
|
135
134
|
userId,
|
|
136
135
|
});
|
|
137
136
|
} else {
|
|
138
|
-
|
|
137
|
+
addPackageBreadcrumb("subscription", "Purchase cancelled", {
|
|
139
138
|
packageId: pkg.identifier,
|
|
140
139
|
userId,
|
|
141
140
|
});
|
|
@@ -178,7 +177,7 @@ export const useRestorePurchase = (userId: string | undefined) => {
|
|
|
178
177
|
throw new Error("User not authenticated");
|
|
179
178
|
}
|
|
180
179
|
|
|
181
|
-
|
|
180
|
+
addPackageBreadcrumb("subscription", "Restore started", {
|
|
182
181
|
userId,
|
|
183
182
|
});
|
|
184
183
|
|
|
@@ -189,7 +188,7 @@ export const useRestorePurchase = (userId: string | undefined) => {
|
|
|
189
188
|
const success = await SubscriptionManager.restore();
|
|
190
189
|
|
|
191
190
|
if (success) {
|
|
192
|
-
|
|
191
|
+
addPackageBreadcrumb("subscription", "Restore success", {
|
|
193
192
|
userId,
|
|
194
193
|
});
|
|
195
194
|
|