@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.10.16",
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": ">=4.0.0"
45
+ "react-native-safe-area-context": ">=5.0.0"
46
46
  },
47
47
  "devDependencies": {
48
- "@umituz/react-native-design-system": "latest",
49
- "@umituz/react-native-firebase": "latest",
50
- "@umituz/react-native-legal": "latest",
51
- "@umituz/react-native-localization": "latest",
52
- "@umituz/react-native-sentry": "latest",
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": "^4.0.0",
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,
@@ -40,7 +40,7 @@ export class CreditsRepository extends BaseRepository {
40
40
  }
41
41
 
42
42
  async getCredits(userId: string): Promise<CreditsResult> {
43
- const db = this.getDb();
43
+ const db = getFirestore();
44
44
  if (!db) {
45
45
  return {
46
46
  success: false,
@@ -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, variant = "bottom-sheet" }) => {
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={containerStyle}>
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, useResponsive } from "@umituz/react-native-design-system";
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, modalLayout, creditsPackages?.length, subscriptionPackages?.length]);
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, useResponsive } from "@umituz/react-native-design-system";
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
- trackPackageEvent("subscription", "purchase_started", {
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
- trackPackageEvent("subscription", "purchase_success", {
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
- trackPackageEvent("subscription", "purchase_cancelled", {
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
- trackPackageEvent("subscription", "restore_started", {
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
- trackPackageEvent("subscription", "restore_success", {
191
+ addPackageBreadcrumb("subscription", "Restore success", {
193
192
  userId,
194
193
  });
195
194