@umituz/react-native-subscription 2.14.83 → 2.14.85

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.83",
3
+ "version": "2.14.85",
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",
@@ -5,6 +5,8 @@
5
5
  * Follows the same pattern as SubscriptionError.
6
6
  */
7
7
 
8
+ import { WALLET_ERROR_MESSAGES } from "./WalletErrorMessages";
9
+
8
10
  export type WalletErrorCategory =
9
11
  | "PAYMENT"
10
12
  | "VALIDATION"
@@ -39,110 +41,100 @@ export abstract class WalletError extends Error {
39
41
  export class PaymentValidationError extends WalletError {
40
42
  readonly code = "PAYMENT_VALIDATION_ERROR";
41
43
  readonly category = "PAYMENT" as const;
42
- readonly userMessage: string;
44
+ readonly userMessage = WALLET_ERROR_MESSAGES.PAYMENT_VALIDATION_FAILED;
43
45
 
44
46
  constructor(message: string, cause?: Error) {
45
47
  super(message, cause);
46
- this.userMessage = "Payment validation failed. Please try again.";
47
48
  }
48
49
  }
49
50
 
50
51
  export class PaymentProviderError extends WalletError {
51
52
  readonly code = "PAYMENT_PROVIDER_ERROR";
52
53
  readonly category = "PAYMENT" as const;
53
- readonly userMessage: string;
54
+ readonly userMessage = WALLET_ERROR_MESSAGES.PAYMENT_PROVIDER_ERROR;
54
55
 
55
56
  constructor(message: string, cause?: Error) {
56
57
  super(message, cause);
57
- this.userMessage = "Payment provider error. Please try again.";
58
58
  }
59
59
  }
60
60
 
61
61
  export class DuplicatePaymentError extends WalletError {
62
62
  readonly code = "DUPLICATE_PAYMENT";
63
63
  readonly category = "PAYMENT" as const;
64
- readonly userMessage: string;
64
+ readonly userMessage = WALLET_ERROR_MESSAGES.DUPLICATE_PAYMENT;
65
65
 
66
66
  constructor(message: string) {
67
67
  super(message);
68
- this.userMessage = "This payment has already been processed.";
69
68
  }
70
69
  }
71
70
 
72
71
  export class UserValidationError extends WalletError {
73
72
  readonly code = "USER_VALIDATION_ERROR";
74
73
  readonly category = "VALIDATION" as const;
75
- readonly userMessage: string;
74
+ readonly userMessage = WALLET_ERROR_MESSAGES.USER_VALIDATION_FAILED;
76
75
 
77
76
  constructor(message: string) {
78
77
  super(message);
79
- this.userMessage = "Invalid user information. Please log in again.";
80
78
  }
81
79
  }
82
80
 
83
81
  export class PackageValidationError extends WalletError {
84
82
  readonly code = "PACKAGE_VALIDATION_ERROR";
85
83
  readonly category = "VALIDATION" as const;
86
- readonly userMessage: string;
84
+ readonly userMessage = WALLET_ERROR_MESSAGES.PACKAGE_VALIDATION_FAILED;
87
85
 
88
86
  constructor(message: string) {
89
87
  super(message);
90
- this.userMessage = "Invalid credit package. Please select a valid package.";
91
88
  }
92
89
  }
93
90
 
94
91
  export class ReceiptValidationError extends WalletError {
95
92
  readonly code = "RECEIPT_VALIDATION_ERROR";
96
93
  readonly category = "VALIDATION" as const;
97
- readonly userMessage: string;
94
+ readonly userMessage = WALLET_ERROR_MESSAGES.RECEIPT_VALIDATION_FAILED;
98
95
 
99
96
  constructor(message: string) {
100
97
  super(message);
101
- this.userMessage = "Invalid payment receipt. Please contact support.";
102
98
  }
103
99
  }
104
100
 
105
101
  export class TransactionError extends WalletError {
106
102
  readonly code = "TRANSACTION_ERROR";
107
103
  readonly category = "INFRASTRUCTURE" as const;
108
- readonly userMessage: string;
104
+ readonly userMessage = WALLET_ERROR_MESSAGES.TRANSACTION_FAILED;
109
105
 
110
106
  constructor(message: string, cause?: Error) {
111
107
  super(message, cause);
112
- this.userMessage = "Transaction failed. Please try again.";
113
108
  }
114
109
  }
115
110
 
116
111
  export class NetworkError extends WalletError {
117
112
  readonly code = "NETWORK_ERROR";
118
113
  readonly category = "INFRASTRUCTURE" as const;
119
- readonly userMessage: string;
114
+ readonly userMessage = WALLET_ERROR_MESSAGES.NETWORK_ERROR;
120
115
 
121
116
  constructor(message: string, cause?: Error) {
122
117
  super(message, cause);
123
- this.userMessage = "Network error. Please check your connection.";
124
118
  }
125
119
  }
126
120
 
127
121
  export class CreditLimitError extends WalletError {
128
122
  readonly code = "CREDIT_LIMIT_ERROR";
129
123
  readonly category = "BUSINESS" as const;
130
- readonly userMessage: string;
124
+ readonly userMessage = WALLET_ERROR_MESSAGES.CREDIT_LIMIT_EXCEEDED;
131
125
 
132
126
  constructor(message: string) {
133
127
  super(message);
134
- this.userMessage = "Credit limit exceeded. Please contact support.";
135
128
  }
136
129
  }
137
130
 
138
131
  export class RefundError extends WalletError {
139
132
  readonly code = "REFUND_ERROR";
140
133
  readonly category = "BUSINESS" as const;
141
- readonly userMessage: string;
134
+ readonly userMessage = WALLET_ERROR_MESSAGES.REFUND_FAILED;
142
135
 
143
136
  constructor(message: string, cause?: Error) {
144
137
  super(message, cause);
145
- this.userMessage = "Refund failed. Please contact support.";
146
138
  }
147
139
  }
148
140
 
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Wallet Error Messages
3
+ * Centralized error user messages for wallet operations
4
+ */
5
+
6
+ export const WALLET_ERROR_MESSAGES = {
7
+ PAYMENT_VALIDATION_FAILED: "Payment validation failed. Please try again.",
8
+ PAYMENT_PROVIDER_ERROR: "Payment provider error. Please try again.",
9
+ DUPLICATE_PAYMENT: "This payment has already been processed.",
10
+ USER_VALIDATION_FAILED: "Invalid user information. Please log in again.",
11
+ PACKAGE_VALIDATION_FAILED: "Invalid credit package. Please select a valid package.",
12
+ RECEIPT_VALIDATION_FAILED: "Invalid payment receipt. Please contact support.",
13
+ TRANSACTION_FAILED: "Transaction failed. Please try again.",
14
+ NETWORK_ERROR: "Network error. Please check your connection.",
15
+ CREDIT_LIMIT_EXCEEDED: "Credit limit exceeded. Please contact support.",
16
+ REFUND_FAILED: "Refund failed. Please contact support.",
17
+ } as const;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Premium Details Card Styles
3
+ * StyleSheet for PremiumDetailsCard component
4
+ */
5
+
6
+ import { StyleSheet } from "react-native";
7
+
8
+ export const styles = StyleSheet.create({
9
+ card: {
10
+ borderRadius: 12,
11
+ padding: 16,
12
+ gap: 12,
13
+ },
14
+ header: {
15
+ flexDirection: "row",
16
+ justifyContent: "space-between",
17
+ alignItems: "center",
18
+ },
19
+ headerTitleContainer: {
20
+ flex: 1,
21
+ marginRight: 12,
22
+ },
23
+ freeUserHeader: {
24
+ marginBottom: 4,
25
+ },
26
+ freeUserTextContainer: {
27
+ gap: 6,
28
+ },
29
+ premiumButton: {
30
+ paddingVertical: 16,
31
+ borderRadius: 12,
32
+ alignItems: "center",
33
+ },
34
+ detailsSection: {
35
+ gap: 8,
36
+ },
37
+ sectionTitle: {
38
+ marginBottom: 4,
39
+ fontWeight: "600",
40
+ },
41
+ creditsSection: {
42
+ gap: 8,
43
+ paddingTop: 12,
44
+ borderTopWidth: 1,
45
+ },
46
+ actionsSection: {
47
+ gap: 8,
48
+ },
49
+ secondaryButton: {
50
+ paddingVertical: 12,
51
+ borderRadius: 8,
52
+ alignItems: "center",
53
+ },
54
+ });
@@ -5,11 +5,12 @@
5
5
  */
6
6
 
7
7
  import React from "react";
8
- import { View, StyleSheet, TouchableOpacity } from "react-native";
8
+ import { View, TouchableOpacity } from "react-native";
9
9
  import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
10
10
  import { PremiumStatusBadge } from "./PremiumStatusBadge";
11
11
  import { DetailRow } from "./DetailRow";
12
12
  import { CreditRow } from "./CreditRow";
13
+ import { styles } from "./PremiumDetailsCard.styles";
13
14
  import type { PremiumDetailsCardProps } from "./PremiumDetailsCardTypes";
14
15
 
15
16
  export type { CreditInfo, PremiumDetailsTranslations, PremiumDetailsCardProps } from "./PremiumDetailsCardTypes";
@@ -144,51 +145,3 @@ export const PremiumDetailsCard: React.FC<PremiumDetailsCardProps> = ({
144
145
  </View>
145
146
  );
146
147
  };
147
-
148
- const styles = StyleSheet.create({
149
- card: {
150
- borderRadius: 12,
151
- padding: 16,
152
- gap: 12,
153
- },
154
- header: {
155
- flexDirection: "row",
156
- justifyContent: "space-between",
157
- alignItems: "center",
158
- },
159
- headerTitleContainer: {
160
- flex: 1,
161
- marginRight: 12,
162
- },
163
- freeUserHeader: {
164
- marginBottom: 4,
165
- },
166
- freeUserTextContainer: {
167
- gap: 6,
168
- },
169
- premiumButton: {
170
- paddingVertical: 16,
171
- borderRadius: 12,
172
- alignItems: "center",
173
- },
174
- detailsSection: {
175
- gap: 8,
176
- },
177
- sectionTitle: {
178
- marginBottom: 4,
179
- fontWeight: "600",
180
- },
181
- creditsSection: {
182
- gap: 8,
183
- paddingTop: 12,
184
- borderTopWidth: 1,
185
- },
186
- actionsSection: {
187
- gap: 8,
188
- },
189
- secondaryButton: {
190
- paddingVertical: 12,
191
- borderRadius: 8,
192
- alignItems: "center",
193
- },
194
- });
@@ -15,7 +15,7 @@ const RERENDER_DELAY_MS = 100;
15
15
  export function usePaywallOperations({
16
16
  userId,
17
17
  isAnonymous,
18
- onPaywallClose,
18
+ onPaywallClose: _onPaywallClose,
19
19
  onPurchaseSuccess,
20
20
  onAuthRequired,
21
21
  }: PaywallOperationsProps): PaywallOperationsResult {
@@ -54,7 +54,7 @@ export function usePaywallOperations({
54
54
  else showError();
55
55
  return success;
56
56
  },
57
- [isAuthenticated, purchasePackage, onPurchaseSuccess, onPaywallClose, onAuthRequired, showError]
57
+ [isAuthenticated, purchasePackage, onPurchaseSuccess, onAuthRequired, showError]
58
58
  );
59
59
 
60
60
  const handleRestore = useCallback(async (): Promise<boolean> => {
@@ -4,9 +4,13 @@
4
4
  */
5
5
 
6
6
  import { useState, useCallback } from 'react';
7
- import { getSubscriptionService } from '../../infrastructure/services/SubscriptionService';
8
7
  import type { SubscriptionStatus } from '../../domain/entities/SubscriptionStatus';
9
8
  import { isSubscriptionValid } from '../../domain/entities/SubscriptionStatus';
9
+ import {
10
+ checkSubscriptionService,
11
+ validateUserId,
12
+ executeSubscriptionOperation,
13
+ } from './useSubscription.utils';
10
14
 
11
15
  export interface UseSubscriptionResult {
12
16
  /** Current subscription status */
@@ -45,120 +49,96 @@ export function useSubscription(): UseSubscriptionResult {
45
49
  const [error, setError] = useState<string | null>(null);
46
50
 
47
51
  const loadStatus = useCallback(async (userId: string) => {
48
- if (!userId) {
49
- setError('User ID is required');
52
+ const validationError = validateUserId(userId);
53
+ if (validationError) {
54
+ setError(validationError);
50
55
  return;
51
56
  }
52
57
 
53
- const service = getSubscriptionService();
54
- if (!service) {
55
- setError('Subscription service is not initialized');
58
+ const serviceCheck = checkSubscriptionService();
59
+ if (!serviceCheck.success) {
60
+ setError(serviceCheck.error || "Service error");
56
61
  return;
57
62
  }
58
63
 
59
- setLoading(true);
60
- setError(null);
61
-
62
- try {
63
- const subscriptionStatus = await service.getSubscriptionStatus(userId);
64
- setStatus(subscriptionStatus);
65
- } catch (err) {
66
- const errorMessage =
67
- err instanceof Error ? err.message : 'Failed to load subscription status';
68
- setError(errorMessage);
69
- } finally {
70
- setLoading(false);
71
- }
64
+ await executeSubscriptionOperation(
65
+ () => serviceCheck.service!.getSubscriptionStatus(userId),
66
+ setLoading,
67
+ setError,
68
+ (result) => setStatus(result)
69
+ );
72
70
  }, []);
73
71
 
74
72
  const refreshStatus = useCallback(async (userId: string) => {
75
- if (!userId) {
76
- setError('User ID is required');
73
+ const validationError = validateUserId(userId);
74
+ if (validationError) {
75
+ setError(validationError);
77
76
  return;
78
77
  }
79
78
 
80
- const service = getSubscriptionService();
81
- if (!service) {
82
- setError('Subscription service is not initialized');
79
+ const serviceCheck = checkSubscriptionService();
80
+ if (!serviceCheck.success) {
81
+ setError(serviceCheck.error || "Service error");
83
82
  return;
84
83
  }
85
84
 
86
- setLoading(true);
87
- setError(null);
88
-
89
- try {
90
- const subscriptionStatus = await service.getSubscriptionStatus(userId);
91
- setStatus(subscriptionStatus);
92
- } catch (err) {
93
- const errorMessage =
94
- err instanceof Error ? err.message : 'Failed to refresh subscription status';
95
- setError(errorMessage);
96
- } finally {
97
- setLoading(false);
98
- }
85
+ await executeSubscriptionOperation(
86
+ () => serviceCheck.service!.getSubscriptionStatus(userId),
87
+ setLoading,
88
+ setError,
89
+ (result) => setStatus(result)
90
+ );
99
91
  }, []);
100
92
 
101
93
  const activateSubscription = useCallback(
102
94
  async (userId: string, productId: string, expiresAt: string | null) => {
103
- if (!userId || !productId) {
104
- setError('User ID and Product ID are required');
95
+ const validationError = validateUserId(userId);
96
+ if (validationError) {
97
+ setError(validationError);
105
98
  return;
106
99
  }
107
100
 
108
- const service = getSubscriptionService();
109
- if (!service) {
110
- setError('Subscription service is not initialized');
101
+ if (!productId) {
102
+ setError("Product ID is required");
111
103
  return;
112
104
  }
113
105
 
114
- setLoading(true);
115
- setError(null);
116
-
117
- try {
118
- const updatedStatus = await service.activateSubscription(
119
- userId,
120
- productId,
121
- expiresAt,
122
- );
123
- setStatus(updatedStatus);
124
- } catch (err) {
125
- const errorMessage =
126
- err instanceof Error ? err.message : 'Failed to activate subscription';
127
- setError(errorMessage);
128
- throw err;
129
- } finally {
130
- setLoading(false);
106
+ const serviceCheck = checkSubscriptionService();
107
+ if (!serviceCheck.success) {
108
+ setError(serviceCheck.error || "Service error");
109
+ return;
131
110
  }
111
+
112
+ await executeSubscriptionOperation(
113
+ () =>
114
+ serviceCheck.service!.activateSubscription(userId, productId, expiresAt),
115
+ setLoading,
116
+ setError,
117
+ (result) => setStatus(result)
118
+ );
132
119
  },
133
- [],
120
+ []
134
121
  );
135
122
 
136
123
  const deactivateSubscription = useCallback(async (userId: string) => {
137
- if (!userId) {
138
- setError('User ID is required');
124
+ const validationError = validateUserId(userId);
125
+ if (validationError) {
126
+ setError(validationError);
139
127
  return;
140
128
  }
141
129
 
142
- const service = getSubscriptionService();
143
- if (!service) {
144
- setError('Subscription service is not initialized');
130
+ const serviceCheck = checkSubscriptionService();
131
+ if (!serviceCheck.success) {
132
+ setError(serviceCheck.error || "Service error");
145
133
  return;
146
134
  }
147
135
 
148
- setLoading(true);
149
- setError(null);
150
-
151
- try {
152
- const updatedStatus = await service.deactivateSubscription(userId);
153
- setStatus(updatedStatus);
154
- } catch (err) {
155
- const errorMessage =
156
- err instanceof Error ? err.message : 'Failed to deactivate subscription';
157
- setError(errorMessage);
158
- throw err;
159
- } finally {
160
- setLoading(false);
161
- }
136
+ await executeSubscriptionOperation(
137
+ () => serviceCheck.service!.deactivateSubscription(userId),
138
+ setLoading,
139
+ setError,
140
+ (result) => setStatus(result)
141
+ );
162
142
  }, []);
163
143
 
164
144
  const isPremium = isSubscriptionValid(status);
@@ -0,0 +1,78 @@
1
+ /**
2
+ * useSubscription Utilities
3
+ * Shared utilities for subscription hook operations
4
+ */
5
+
6
+ export type AsyncSubscriptionOperation<T> = () => Promise<T>;
7
+
8
+ /**
9
+ * Result of a subscription service initialization check
10
+ */
11
+ export interface ServiceCheckResult {
12
+ success: boolean;
13
+ service: ReturnType<typeof import("../../infrastructure/services/SubscriptionService").getSubscriptionService> | null;
14
+ error?: string;
15
+ }
16
+
17
+ /**
18
+ * Checks if subscription service is initialized
19
+ * Returns service instance or error
20
+ */
21
+ export function checkSubscriptionService(): ServiceCheckResult {
22
+ const { getSubscriptionService } = require("../../infrastructure/services/SubscriptionService");
23
+ const service = getSubscriptionService();
24
+
25
+ if (!service) {
26
+ return {
27
+ success: false,
28
+ service: null,
29
+ error: "Subscription service is not initialized",
30
+ };
31
+ }
32
+
33
+ return { success: true, service, error: undefined };
34
+ }
35
+
36
+ /**
37
+ * Validates user ID
38
+ */
39
+ export function validateUserId(userId: string): string | null {
40
+ if (!userId) {
41
+ return "User ID is required";
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * Wraps async subscription operations with loading state, error handling, and state updates
48
+ */
49
+ export async function executeSubscriptionOperation<T>(
50
+ operation: AsyncSubscriptionOperation<T>,
51
+ setLoading: (loading: boolean) => void,
52
+ setError: (error: string | null) => void,
53
+ onSuccess?: (result: T) => void
54
+ ): Promise<void> {
55
+ setLoading(true);
56
+ setError(null);
57
+
58
+ try {
59
+ const result = await operation();
60
+ if (onSuccess) {
61
+ onSuccess(result);
62
+ }
63
+ } catch (err) {
64
+ const errorMessage =
65
+ err instanceof Error ? err.message : "Operation failed";
66
+ setError(errorMessage);
67
+ throw err;
68
+ } finally {
69
+ setLoading(false);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Formats error message from unknown error
75
+ */
76
+ export function formatErrorMessage(err: unknown, fallbackMessage: string): string {
77
+ return err instanceof Error ? err.message : fallbackMessage;
78
+ }
@@ -15,6 +15,7 @@ import {
15
15
  formatDateForLocale,
16
16
  calculateDaysRemaining,
17
17
  } from "../utils/subscriptionDateUtils";
18
+ import { useCreditsArray, getSubscriptionStatusType } from "./useSubscriptionSettingsConfig.utils";
18
19
  import type {
19
20
  SubscriptionSettingsConfig,
20
21
  SubscriptionStatusType,
@@ -104,23 +105,10 @@ export const useSubscriptionSettingsConfig = (
104
105
  );
105
106
 
106
107
  // Status type
107
- const statusType: SubscriptionStatusType = isPremium ? "active" : "none";
108
+ const statusType: SubscriptionStatusType = getSubscriptionStatusType(isPremium);
108
109
 
109
110
  // Credits array
110
- const creditsArray = useMemo(() => {
111
- if (!credits) return [];
112
- const total = getCreditLimit
113
- ? getCreditLimit(credits.imageCredits)
114
- : credits.imageCredits;
115
- return [
116
- {
117
- id: "image",
118
- label: translations.imageCreditsLabel || "Image Credits",
119
- current: credits.imageCredits,
120
- total,
121
- },
122
- ];
123
- }, [credits, getCreditLimit, translations.imageCreditsLabel]);
111
+ const creditsArray = useCreditsArray(credits, getCreditLimit, translations);
124
112
 
125
113
  // Build config
126
114
  const config = useMemo(
@@ -0,0 +1,48 @@
1
+ /**
2
+ * useSubscriptionSettingsConfig Utilities
3
+ * Helper functions for subscription settings config
4
+ */
5
+
6
+ import { useMemo } from "react";
7
+ import type { UserCredits } from "../../domain/entities/Credits";
8
+ import type { SubscriptionSettingsTranslations } from "../types/SubscriptionSettingsTypes";
9
+
10
+ export interface CreditsInfo {
11
+ id: string;
12
+ label: string;
13
+ current: number;
14
+ total: number;
15
+ }
16
+
17
+ /**
18
+ * Builds credits array for display
19
+ */
20
+ export function useCreditsArray(
21
+ credits: UserCredits | null | undefined,
22
+ getCreditLimit: ((credits: number) => number) | undefined,
23
+ translations: SubscriptionSettingsTranslations
24
+ ): CreditsInfo[] {
25
+ return useMemo(() => {
26
+ if (!credits) return [];
27
+ const total = getCreditLimit
28
+ ? getCreditLimit(credits.imageCredits)
29
+ : credits.imageCredits;
30
+ return [
31
+ {
32
+ id: "image",
33
+ label: translations.imageCreditsLabel || "Image Credits",
34
+ current: credits.imageCredits,
35
+ total,
36
+ },
37
+ ];
38
+ }, [credits, getCreditLimit, translations.imageCreditsLabel]);
39
+ }
40
+
41
+ /**
42
+ * Calculates subscription status type
43
+ */
44
+ export function getSubscriptionStatusType(
45
+ isPremium: boolean
46
+ ): "active" | "none" {
47
+ return isPremium ? "active" : "none";
48
+ }