@umituz/react-native-subscription 2.14.86 → 2.14.87

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.14.86",
3
+ "version": "2.14.87",
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",
@@ -34,7 +34,8 @@
34
34
  "dependencies": {
35
35
  "@umituz/react-native-design-system": "latest",
36
36
  "@umituz/react-native-firebase": "latest",
37
- "@umituz/react-native-localization": "latest"
37
+ "@umituz/react-native-localization": "latest",
38
+ "@umituz/react-native-storage": "latest"
38
39
  },
39
40
  "peerDependencies": {
40
41
  "@tanstack/react-query": ">=5.0.0",
@@ -48,27 +49,53 @@
48
49
  "react-native-safe-area-context": ">=5.0.0"
49
50
  },
50
51
  "devDependencies": {
52
+ "@expo/vector-icons": "^15.0.3",
53
+ "@gorhom/bottom-sheet": "^5.2.8",
54
+ "@react-native-async-storage/async-storage": "^2.2.0",
55
+ "@react-native-community/datetimepicker": "^8.6.0",
56
+ "@react-navigation/bottom-tabs": "^7.9.0",
57
+ "@react-navigation/native": "^7.1.26",
58
+ "@react-navigation/stack": "^7.6.13",
51
59
  "@tanstack/react-query": "^5.0.0",
52
60
  "@types/react": "~19.1.10",
53
61
  "@typescript-eslint/eslint-plugin": "^8.50.1",
54
62
  "@typescript-eslint/parser": "^8.50.1",
55
63
  "@umituz/react-native-auth": "*",
56
- "@umituz/react-native-storage": "*",
57
64
  "@umituz/react-native-design-system": "*",
65
+ "@umituz/react-native-filesystem": "^2.1.20",
58
66
  "@umituz/react-native-firebase": "*",
67
+ "@umituz/react-native-haptics": "^1.0.5",
59
68
  "@umituz/react-native-localization": "*",
69
+ "@umituz/react-native-storage": "*",
70
+ "@umituz/react-native-uuid": "^1.2.6",
60
71
  "eslint": "^9.39.2",
61
72
  "eslint-plugin-react": "^7.37.5",
62
73
  "eslint-plugin-react-hooks": "^7.0.1",
74
+ "expo-apple-authentication": "^8.0.8",
75
+ "expo-application": "^7.0.8",
76
+ "expo-clipboard": "^8.0.8",
63
77
  "expo-constants": "~16.0.0",
78
+ "expo-crypto": "^15.0.8",
79
+ "expo-device": "^8.0.10",
80
+ "expo-file-system": "^19.0.21",
81
+ "expo-haptics": "^15.0.8",
64
82
  "expo-image": "~3.0.0",
65
83
  "expo-linear-gradient": "~15.0.0",
84
+ "expo-localization": "^17.0.8",
85
+ "expo-sharing": "^14.0.8",
66
86
  "firebase": "^11.0.0",
87
+ "i18next": "^25.7.3",
67
88
  "react": "19.1.0",
89
+ "react-i18next": "^16.5.1",
68
90
  "react-native": "0.81.5",
91
+ "react-native-gesture-handler": "^2.30.0",
69
92
  "react-native-purchases": "^7.0.0",
93
+ "react-native-reanimated": "^4.2.1",
70
94
  "react-native-safe-area-context": "^5.0.0",
71
- "typescript": "~5.9.2"
95
+ "react-native-svg": "^15.15.1",
96
+ "rn-emoji-keyboard": "^1.7.0",
97
+ "typescript": "~5.9.2",
98
+ "zustand": "^5.0.9"
72
99
  },
73
100
  "publishConfig": {
74
101
  "access": "public"
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Transaction Mapper
3
+ * Maps Firestore data to CreditLog entity
4
+ */
5
+
6
+ import type { QueryDocumentSnapshot, DocumentData } from "firebase/firestore";
7
+ import type { CreditLog } from "../types/transaction.types";
8
+
9
+ export class TransactionMapper {
10
+ static toEntity(docSnap: QueryDocumentSnapshot<DocumentData>, defaultUserId: string): CreditLog {
11
+ const data = docSnap.data();
12
+ return {
13
+ id: docSnap.id,
14
+ userId: data.userId || defaultUserId,
15
+ change: data.change,
16
+ reason: data.reason,
17
+ feature: data.feature,
18
+ jobId: data.jobId,
19
+ packageId: data.packageId,
20
+ subscriptionPlan: data.subscriptionPlan,
21
+ description: data.description,
22
+ createdAt: data.createdAt?.toMillis?.() || Date.now(),
23
+ };
24
+ }
25
+
26
+ static toFirestore(userId: string, change: number, reason: string, metadata?: Partial<CreditLog>) {
27
+ return {
28
+ userId,
29
+ change,
30
+ reason,
31
+ feature: metadata?.feature,
32
+ jobId: metadata?.jobId,
33
+ packageId: metadata?.packageId,
34
+ subscriptionPlan: metadata?.subscriptionPlan,
35
+ description: metadata?.description,
36
+ };
37
+ }
38
+ }
@@ -25,6 +25,7 @@ import type {
25
25
  TransactionResult,
26
26
  TransactionReason,
27
27
  } from "../../domain/types/transaction.types";
28
+ import { TransactionMapper } from "../../domain/mappers/TransactionMapper";
28
29
 
29
30
  export class TransactionRepository extends BaseRepository {
30
31
  private config: TransactionRepositoryConfig;
@@ -66,21 +67,9 @@ export class TransactionRepository extends BaseRepository {
66
67
  const q = query(colRef, ...constraints);
67
68
  const snapshot = await getDocs(q);
68
69
 
69
- const transactions: CreditLog[] = snapshot.docs.map((docSnap) => {
70
- const data = docSnap.data();
71
- return {
72
- id: docSnap.id,
73
- userId: data.userId || options.userId,
74
- change: data.change,
75
- reason: data.reason,
76
- feature: data.feature,
77
- jobId: data.jobId,
78
- packageId: data.packageId,
79
- subscriptionPlan: data.subscriptionPlan,
80
- description: data.description,
81
- createdAt: data.createdAt?.toMillis?.() || Date.now(),
82
- };
83
- });
70
+ const transactions: CreditLog[] = snapshot.docs.map((docSnap) =>
71
+ TransactionMapper.toEntity(docSnap, options.userId)
72
+ );
84
73
 
85
74
  return { success: true, data: transactions };
86
75
  } catch (error) {
@@ -112,14 +101,7 @@ export class TransactionRepository extends BaseRepository {
112
101
  try {
113
102
  const colRef = this.getCollectionRef(db, userId);
114
103
  const docData = {
115
- userId,
116
- change,
117
- reason,
118
- feature: metadata?.feature,
119
- jobId: metadata?.jobId,
120
- packageId: metadata?.packageId,
121
- subscriptionPlan: metadata?.subscriptionPlan,
122
- description: metadata?.description,
104
+ ...TransactionMapper.toFirestore(userId, change, reason, metadata),
123
105
  createdAt: serverTimestamp(),
124
106
  };
125
107
 
@@ -18,7 +18,7 @@ export const CreditRow: React.FC<CreditRowProps> = ({
18
18
  label,
19
19
  current,
20
20
  total,
21
- remainingLabel = "remaining",
21
+ remainingLabel,
22
22
  }) => {
23
23
  const tokens = useAppDesignTokens();
24
24
  const percentage = total > 0 ? (current / total) * 100 : 0;
@@ -27,7 +27,7 @@ export const CreditRow: React.FC<CreditRowProps> = ({
27
27
  return (
28
28
  <View style={styles.container}>
29
29
  <View style={styles.header}>
30
- <AtomicText type="bodySmall" style={{ color: tokens.colors.text }}>
30
+ <AtomicText type="bodySmall" style={{ color: tokens.colors.textPrimary }}>
31
31
  {label}
32
32
  </AtomicText>
33
33
  <AtomicText
@@ -4,33 +4,45 @@
4
4
  */
5
5
 
6
6
  import React from "react";
7
- import { View, StyleSheet } from "react-native";
7
+ import { View, StyleSheet, type ViewStyle, type TextStyle } from "react-native";
8
8
  import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
9
9
 
10
10
  export interface DetailRowProps {
11
11
  label: string;
12
12
  value: string;
13
13
  highlight?: boolean;
14
+ style?: ViewStyle;
15
+ labelStyle?: TextStyle;
16
+ valueStyle?: TextStyle;
14
17
  }
15
18
 
16
19
  export const DetailRow: React.FC<DetailRowProps> = ({
17
20
  label,
18
21
  value,
19
22
  highlight = false,
23
+ style,
24
+ labelStyle,
25
+ valueStyle,
20
26
  }) => {
21
27
  const tokens = useAppDesignTokens();
22
28
 
23
29
  return (
24
- <View style={styles.container}>
25
- <AtomicText type="bodyMedium" style={{ color: tokens.colors.textSecondary }}>
30
+ <View style={[styles.container, style]}>
31
+ <AtomicText
32
+ type="bodyMedium"
33
+ style={[{ color: tokens.colors.textSecondary }, labelStyle]}
34
+ >
26
35
  {label}
27
36
  </AtomicText>
28
37
  <AtomicText
29
38
  type="bodyMedium"
30
- style={{
31
- color: highlight ? tokens.colors.warning : tokens.colors.text,
32
- fontWeight: "500",
33
- }}
39
+ style={[
40
+ {
41
+ color: highlight ? tokens.colors.warning : tokens.colors.textPrimary,
42
+ fontWeight: "500",
43
+ },
44
+ valueStyle,
45
+ ]}
34
46
  >
35
47
  {value}
36
48
  </AtomicText>
@@ -48,98 +48,60 @@ export function useSubscription(): UseSubscriptionResult {
48
48
  const [loading, setLoading] = useState(false);
49
49
  const [error, setError] = useState<string | null>(null);
50
50
 
51
- const loadStatus = useCallback(async (userId: string) => {
52
- const validationError = validateUserId(userId);
53
- if (validationError) {
54
- setError(validationError);
51
+ const performOperation = useCallback(async (
52
+ userId: string,
53
+ operation: () => Promise<SubscriptionStatus | null | void>
54
+ ) => {
55
+ const errorMsg = validateUserId(userId);
56
+ if (errorMsg) {
57
+ setError(errorMsg);
55
58
  return;
56
59
  }
57
60
 
58
- const serviceCheck = checkSubscriptionService();
59
- if (!serviceCheck.success) {
60
- setError(serviceCheck.error || "Service error");
61
+ const check = checkSubscriptionService();
62
+ if (!check.success) {
63
+ setError(check.error || "Service error");
61
64
  return;
62
65
  }
63
66
 
64
67
  await executeSubscriptionOperation(
65
- () => serviceCheck.service!.getSubscriptionStatus(userId),
68
+ operation,
66
69
  setLoading,
67
70
  setError,
68
- (result) => setStatus(result)
71
+ (result) => { if (result) setStatus(result as SubscriptionStatus); }
69
72
  );
70
73
  }, []);
71
74
 
72
- const refreshStatus = useCallback(async (userId: string) => {
73
- const validationError = validateUserId(userId);
74
- if (validationError) {
75
- setError(validationError);
76
- return;
77
- }
75
+ const loadStatus = useCallback((userId: string) =>
76
+ performOperation(userId, () => {
77
+ const { service } = checkSubscriptionService();
78
+
79
+ return service!.getSubscriptionStatus(userId);
80
+ }), [performOperation]);
78
81
 
79
- const serviceCheck = checkSubscriptionService();
80
- if (!serviceCheck.success) {
81
- setError(serviceCheck.error || "Service error");
82
- return;
83
- }
84
-
85
- await executeSubscriptionOperation(
86
- () => serviceCheck.service!.getSubscriptionStatus(userId),
87
- setLoading,
88
- setError,
89
- (result) => setStatus(result)
90
- );
91
- }, []);
82
+ const refreshStatus = loadStatus;
92
83
 
93
84
  const activateSubscription = useCallback(
94
- async (userId: string, productId: string, expiresAt: string | null) => {
95
- const validationError = validateUserId(userId);
96
- if (validationError) {
97
- setError(validationError);
98
- return;
99
- }
100
-
85
+ (userId: string, productId: string, expiresAt: string | null) => {
101
86
  if (!productId) {
102
87
  setError("Product ID is required");
103
- return;
88
+ return Promise.resolve();
104
89
  }
105
-
106
- const serviceCheck = checkSubscriptionService();
107
- if (!serviceCheck.success) {
108
- setError(serviceCheck.error || "Service error");
109
- return;
110
- }
111
-
112
- await executeSubscriptionOperation(
113
- () =>
114
- serviceCheck.service!.activateSubscription(userId, productId, expiresAt),
115
- setLoading,
116
- setError,
117
- (result) => setStatus(result)
118
- );
90
+ return performOperation(userId, () => {
91
+ const { service } = checkSubscriptionService();
92
+
93
+ return service!.activateSubscription(userId, productId, expiresAt).then(res => res ?? undefined);
94
+ });
119
95
  },
120
- []
96
+ [performOperation]
121
97
  );
122
98
 
123
- const deactivateSubscription = useCallback(async (userId: string) => {
124
- const validationError = validateUserId(userId);
125
- if (validationError) {
126
- setError(validationError);
127
- return;
128
- }
129
-
130
- const serviceCheck = checkSubscriptionService();
131
- if (!serviceCheck.success) {
132
- setError(serviceCheck.error || "Service error");
133
- return;
134
- }
135
-
136
- await executeSubscriptionOperation(
137
- () => serviceCheck.service!.deactivateSubscription(userId),
138
- setLoading,
139
- setError,
140
- (result) => setStatus(result)
141
- );
142
- }, []);
99
+ const deactivateSubscription = useCallback((userId: string) =>
100
+ performOperation(userId, () => {
101
+ const { service } = checkSubscriptionService();
102
+
103
+ return service!.deactivateSubscription(userId).then(res => res ?? undefined);
104
+ }), [performOperation]);
143
105
 
144
106
  const isPremium = isSubscriptionValid(status);
145
107
 
@@ -7,6 +7,7 @@ import React, { useMemo } from "react";
7
7
  import { View, StyleSheet } from "react-native";
8
8
  import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
9
9
  import { PremiumStatusBadge } from "../../components/details/PremiumStatusBadge";
10
+ import { DetailRow } from "../../components/details/DetailRow";
10
11
  import type { SubscriptionHeaderProps } from "../../types/SubscriptionDetailTypes";
11
12
 
12
13
  export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
@@ -90,8 +91,9 @@ export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
90
91
  <DetailRow
91
92
  label={translations.statusLabel}
92
93
  value={translations.lifetimeLabel}
93
- tokens={tokens}
94
- styles={styles}
94
+ style={styles.row}
95
+ labelStyle={styles.label}
96
+ valueStyle={styles.value}
95
97
  />
96
98
  ) : (
97
99
  <>
@@ -100,16 +102,18 @@ export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
100
102
  label={translations.expiresLabel}
101
103
  value={expirationDate}
102
104
  highlight={showExpiring}
103
- tokens={tokens}
104
- styles={styles}
105
+ style={styles.row}
106
+ labelStyle={styles.label}
107
+ valueStyle={styles.value}
105
108
  />
106
109
  )}
107
110
  {purchaseDate && (
108
111
  <DetailRow
109
112
  label={translations.purchasedLabel}
110
113
  value={purchaseDate}
111
- tokens={tokens}
112
- styles={styles}
114
+ style={styles.row}
115
+ labelStyle={styles.label}
116
+ valueStyle={styles.value}
113
117
  />
114
118
  )}
115
119
  </>
@@ -119,41 +123,3 @@ export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
119
123
  </View>
120
124
  );
121
125
  };
122
-
123
- interface DetailRowProps {
124
- label: string;
125
- value: string;
126
- highlight?: boolean;
127
- tokens: ReturnType<typeof useAppDesignTokens>;
128
- styles: {
129
- row: object;
130
- label: object;
131
- value: object;
132
- };
133
- }
134
-
135
- const DetailRow: React.FC<DetailRowProps> = ({
136
- label,
137
- value,
138
- highlight,
139
- tokens,
140
- styles,
141
- }) => (
142
- <View style={styles.row}>
143
- <AtomicText
144
- type="bodyMedium"
145
- style={[styles.label, { color: tokens.colors.textSecondary }]}
146
- >
147
- {label}
148
- </AtomicText>
149
- <AtomicText
150
- type="bodyMedium"
151
- style={[
152
- styles.value,
153
- { color: highlight ? tokens.colors.warning : tokens.colors.textPrimary },
154
- ]}
155
- >
156
- {value}
157
- </AtomicText>
158
- </View>
159
- );
@@ -51,6 +51,17 @@ export class RevenueCatService implements IRevenueCatService {
51
51
  return this.stateManager.getCurrentUserId();
52
52
  }
53
53
 
54
+ private getSDKParams() {
55
+ return {
56
+ config: this.stateManager.getConfig(),
57
+ isUsingTestStore: () => this.isUsingTestStore(),
58
+ isInitialized: () => this.isInitialized(),
59
+ getCurrentUserId: () => this.stateManager.getCurrentUserId(),
60
+ setInitialized: (value: boolean) => this.stateManager.setInitialized(value),
61
+ setCurrentUserId: (id: string | null) => this.stateManager.setCurrentUserId(id),
62
+ };
63
+ }
64
+
54
65
  async initialize(userId: string, apiKey?: string): Promise<InitializeResult> {
55
66
  if (this.isInitialized() && this.getCurrentUserId() === userId) {
56
67
  return {
@@ -60,18 +71,7 @@ export class RevenueCatService implements IRevenueCatService {
60
71
  };
61
72
  }
62
73
 
63
- const result = await initializeSDK(
64
- {
65
- config: this.stateManager.getConfig(),
66
- isUsingTestStore: () => this.isUsingTestStore(),
67
- isInitialized: () => this.isInitialized(),
68
- getCurrentUserId: () => this.stateManager.getCurrentUserId(),
69
- setInitialized: (value) => this.stateManager.setInitialized(value),
70
- setCurrentUserId: (id) => this.stateManager.setCurrentUserId(id),
71
- },
72
- userId,
73
- apiKey
74
- );
74
+ const result = await initializeSDK(this.getSDKParams(), userId, apiKey);
75
75
 
76
76
  if (result.success) {
77
77
  this.listenerManager.setUserId(userId);
@@ -82,36 +82,18 @@ export class RevenueCatService implements IRevenueCatService {
82
82
  }
83
83
 
84
84
  async fetchOfferings(): Promise<PurchasesOffering | null> {
85
- return fetchOfferings({
86
- isInitialized: () => this.isInitialized(),
87
- isUsingTestStore: () => this.isUsingTestStore(),
88
- });
85
+ return fetchOfferings(this.getSDKParams());
89
86
  }
90
87
 
91
88
  async purchasePackage(
92
89
  pkg: PurchasesPackage,
93
90
  userId: string
94
91
  ): Promise<PurchaseResult> {
95
- return handlePurchase(
96
- {
97
- config: this.stateManager.getConfig(),
98
- isInitialized: () => this.isInitialized(),
99
- isUsingTestStore: () => this.isUsingTestStore(),
100
- },
101
- pkg,
102
- userId
103
- );
92
+ return handlePurchase(this.getSDKParams(), pkg, userId);
104
93
  }
105
94
 
106
95
  async restorePurchases(userId: string): Promise<RestoreResult> {
107
- return handleRestore(
108
- {
109
- config: this.stateManager.getConfig(),
110
- isInitialized: () => this.isInitialized(),
111
- isUsingTestStore: () => this.isUsingTestStore(),
112
- },
113
- userId
114
- );
96
+ return handleRestore(this.getSDKParams(), userId);
115
97
  }
116
98
 
117
99
  async getCustomerInfo(): Promise<CustomerInfo | null> {
@@ -33,7 +33,7 @@ export class ServiceStateManager {
33
33
  return this.isInitializedFlag;
34
34
  }
35
35
 
36
- setCurrentUserId(userId: string): void {
36
+ setCurrentUserId(userId: string | null): void {
37
37
  this.currentUserId = userId;
38
38
  }
39
39