@umituz/react-native-subscription 3.1.10 → 3.1.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/domains/credits/presentation/useCreditsRealTime.ts +10 -5
- package/src/domains/credits/utils/creditValidation.ts +5 -26
- package/src/domains/paywall/hooks/usePaywallActions.ts +21 -133
- package/src/domains/paywall/hooks/usePaywallActions.types.ts +16 -0
- package/src/domains/paywall/hooks/usePaywallPurchase.ts +78 -0
- package/src/domains/paywall/hooks/usePaywallRestore.ts +66 -0
- package/src/domains/revenuecat/infrastructure/services/userSwitchCore.ts +116 -0
- package/src/domains/revenuecat/infrastructure/services/userSwitchHandler.ts +19 -237
- package/src/domains/revenuecat/infrastructure/services/userSwitchHelpers.ts +55 -0
- package/src/domains/revenuecat/infrastructure/services/userSwitchInitializer.ts +143 -0
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +6 -3
- package/src/domains/subscription/infrastructure/managers/initializationHandler.ts +2 -2
- package/src/domains/subscription/infrastructure/managers/packageHandlerFactory.ts +2 -2
- package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +2 -2
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.logic.ts +52 -0
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.tsx +15 -89
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.types.ts +59 -0
- package/src/domains/subscription/presentation/components/details/CreditRow.tsx +9 -0
- package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.tsx +23 -0
- package/src/domains/subscription/presentation/components/states/FeedbackState.tsx +36 -0
- package/src/domains/subscription/presentation/components/states/InitializingState.tsx +47 -0
- package/src/domains/subscription/presentation/components/states/OnboardingState.tsx +27 -0
- package/src/domains/subscription/presentation/components/states/PaywallState.tsx +66 -0
- package/src/domains/subscription/presentation/components/states/ReadyState.tsx +51 -0
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx +119 -103
- package/src/domains/wallet/presentation/components/BalanceCard.tsx +7 -0
- package/src/domains/wallet/presentation/components/TransactionItem.tsx +11 -0
- package/src/index.components.ts +1 -1
- package/src/shared/infrastructure/SubscriptionEventBus.ts +4 -2
- package/src/shared/presentation/hooks/useFirestoreRealTime.ts +22 -6
- package/src/shared/utils/errors/errorAssertions.ts +35 -0
- package/src/shared/utils/errors/errorConversion.ts +73 -0
- package/src/shared/utils/errors/errorTypeGuards.ts +27 -0
- package/src/shared/utils/errors/errorWrappers.ts +54 -0
- package/src/shared/utils/errors/index.ts +19 -0
- package/src/shared/utils/errors/serviceErrors.ts +36 -0
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.states.tsx +0 -187
- package/src/shared/utils/errorUtils.ts +0 -195
package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx
CHANGED
|
@@ -11,6 +11,12 @@ interface SubscriptionHeaderContentStyles {
|
|
|
11
11
|
value: TextStyle;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
interface DetailInfo {
|
|
15
|
+
label?: string;
|
|
16
|
+
value: string;
|
|
17
|
+
highlight?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
interface SubscriptionHeaderContentProps {
|
|
15
21
|
showExpirationDate: boolean;
|
|
16
22
|
expirationDate?: string;
|
|
@@ -28,106 +34,116 @@ interface SubscriptionHeaderContentProps {
|
|
|
28
34
|
isSandbox?: boolean;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Helper to build detail row data array.
|
|
39
|
+
* Reduces code duplication by centralizing detail row creation logic.
|
|
40
|
+
*/
|
|
41
|
+
function buildDetails(
|
|
42
|
+
props: SubscriptionHeaderContentProps
|
|
43
|
+
): DetailInfo[] {
|
|
44
|
+
const {
|
|
45
|
+
showExpirationDate,
|
|
46
|
+
expirationDate,
|
|
47
|
+
purchaseDate,
|
|
48
|
+
showExpiring,
|
|
49
|
+
translations,
|
|
50
|
+
willRenew,
|
|
51
|
+
packageType,
|
|
52
|
+
store,
|
|
53
|
+
originalPurchaseDate,
|
|
54
|
+
latestPurchaseDate,
|
|
55
|
+
billingIssuesDetected,
|
|
56
|
+
isSandbox,
|
|
57
|
+
} = props;
|
|
58
|
+
|
|
59
|
+
const details: DetailInfo[] = [];
|
|
60
|
+
|
|
61
|
+
if (showExpirationDate && expirationDate) {
|
|
62
|
+
details.push({
|
|
63
|
+
label: translations.expiresLabel,
|
|
64
|
+
value: expirationDate,
|
|
65
|
+
highlight: showExpiring,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (purchaseDate) {
|
|
70
|
+
details.push({
|
|
71
|
+
label: translations.purchasedLabel,
|
|
72
|
+
value: purchaseDate,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (willRenew !== null && willRenew !== undefined && translations.willRenewLabel) {
|
|
77
|
+
details.push({
|
|
78
|
+
label: translations.willRenewLabel,
|
|
79
|
+
value: willRenew ? (translations.willRenewYes ?? "Yes") : (translations.willRenewNo ?? "No"),
|
|
80
|
+
highlight: !willRenew,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (packageType && translations.periodTypeLabel) {
|
|
85
|
+
details.push({
|
|
86
|
+
label: translations.periodTypeLabel,
|
|
87
|
+
value: formatPackageTypeForDisplay(packageType),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (store && translations.storeLabel) {
|
|
92
|
+
details.push({
|
|
93
|
+
label: translations.storeLabel,
|
|
94
|
+
value: store,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (originalPurchaseDate && translations.originalPurchaseDateLabel) {
|
|
99
|
+
details.push({
|
|
100
|
+
label: translations.originalPurchaseDateLabel,
|
|
101
|
+
value: originalPurchaseDate,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (latestPurchaseDate && translations.latestPurchaseDateLabel) {
|
|
106
|
+
details.push({
|
|
107
|
+
label: translations.latestPurchaseDateLabel,
|
|
108
|
+
value: latestPurchaseDate,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (billingIssuesDetected && translations.billingIssuesLabel) {
|
|
113
|
+
details.push({
|
|
114
|
+
label: translations.billingIssuesLabel,
|
|
115
|
+
value: translations.billingIssuesDetected ?? "Detected",
|
|
116
|
+
highlight: true,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__ && isSandbox && translations.sandboxLabel) {
|
|
121
|
+
details.push({
|
|
122
|
+
label: translations.sandboxLabel,
|
|
123
|
+
value: translations.sandboxTestMode ?? "Test Mode",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return details;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const SubscriptionHeaderContent: React.FC<SubscriptionHeaderContentProps> = (props) => {
|
|
131
|
+
const { styles } = props;
|
|
132
|
+
const details = buildDetails(props);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<View style={styles.details}>
|
|
136
|
+
{details.map((detail) => (
|
|
137
|
+
<DetailRow
|
|
138
|
+
key={detail.label}
|
|
139
|
+
label={detail.label!}
|
|
140
|
+
value={detail.value}
|
|
141
|
+
highlight={detail.highlight}
|
|
142
|
+
style={styles.row}
|
|
143
|
+
labelStyle={styles.label}
|
|
144
|
+
valueStyle={styles.value}
|
|
145
|
+
/>
|
|
146
|
+
))}
|
|
147
|
+
</View>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
@@ -58,6 +58,13 @@ export const BalanceCard: React.FC<BalanceCardProps> = React.memo(({
|
|
|
58
58
|
</View>
|
|
59
59
|
</View>
|
|
60
60
|
);
|
|
61
|
+
}, (prevProps, nextProps) => {
|
|
62
|
+
// PERFORMANCE: Custom comparison to prevent unnecessary re-renders
|
|
63
|
+
return (
|
|
64
|
+
prevProps.balance === nextProps.balance &&
|
|
65
|
+
prevProps.translations === nextProps.translations &&
|
|
66
|
+
prevProps.iconName === nextProps.iconName
|
|
67
|
+
);
|
|
61
68
|
});
|
|
62
69
|
|
|
63
70
|
const styles = StyleSheet.create({
|
|
@@ -44,4 +44,15 @@ export const TransactionItem: React.FC<TransactionItemProps> = React.memo(({
|
|
|
44
44
|
</AtomicText>
|
|
45
45
|
</View>
|
|
46
46
|
);
|
|
47
|
+
}, (prevProps, nextProps) => {
|
|
48
|
+
// PERFORMANCE: Custom comparison to prevent unnecessary re-renders
|
|
49
|
+
return (
|
|
50
|
+
prevProps.transaction.id === nextProps.transaction.id &&
|
|
51
|
+
prevProps.transaction.change === nextProps.transaction.change &&
|
|
52
|
+
prevProps.transaction.reason === nextProps.transaction.reason &&
|
|
53
|
+
prevProps.transaction.description === nextProps.transaction.description &&
|
|
54
|
+
prevProps.transaction.createdAt === nextProps.transaction.createdAt &&
|
|
55
|
+
prevProps.translations === nextProps.translations &&
|
|
56
|
+
prevProps.dateFormatter === nextProps.dateFormatter
|
|
57
|
+
);
|
|
47
58
|
});
|
package/src/index.components.ts
CHANGED
|
@@ -35,6 +35,6 @@ export type { PaywallScreenProps } from "./domains/paywall/components/PaywallScr
|
|
|
35
35
|
|
|
36
36
|
// Root Flow Components
|
|
37
37
|
export { ManagedSubscriptionFlow } from "./domains/subscription/presentation/components/ManagedSubscriptionFlow";
|
|
38
|
-
export type { ManagedSubscriptionFlowProps } from "./domains/subscription/presentation/components/ManagedSubscriptionFlow";
|
|
38
|
+
export type { ManagedSubscriptionFlowProps } from "./domains/subscription/presentation/components/ManagedSubscriptionFlow.types";
|
|
39
39
|
export { SubscriptionFlowStatus } from "./domains/subscription/presentation/useSubscriptionFlow";
|
|
40
40
|
export { SubscriptionFlowProvider, useSubscriptionFlowStatus } from "./domains/subscription/presentation/providers/SubscriptionFlowProvider";
|
|
@@ -36,8 +36,10 @@ class SubscriptionEventBus {
|
|
|
36
36
|
const listeners = this.listeners.get(event);
|
|
37
37
|
if (!listeners || listeners.size === 0) return;
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// PERFORMANCE: Batch all callbacks in a single microtask to reduce call stack overhead
|
|
40
|
+
// This prevents UI jank when multiple listeners are registered
|
|
41
|
+
queueMicrotask(() => {
|
|
42
|
+
listeners.forEach(callback => {
|
|
41
43
|
try {
|
|
42
44
|
callback(data);
|
|
43
45
|
} catch (error) {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - Support for both document and collection queries
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { useEffect, useState, useCallback } from "react";
|
|
17
|
+
import { useEffect, useState, useCallback, useRef } from "react";
|
|
18
18
|
import {
|
|
19
19
|
onSnapshot,
|
|
20
20
|
type Query,
|
|
@@ -65,6 +65,8 @@ export type Mapper<TDocument, TEntity> = (
|
|
|
65
65
|
/**
|
|
66
66
|
* Generic hook for real-time document sync via Firestore onSnapshot.
|
|
67
67
|
*
|
|
68
|
+
* PERFORMANCE: Uses useRef to stabilize mapper and prevent unnecessary re-subscriptions
|
|
69
|
+
*
|
|
68
70
|
* @template TDocument - Firestore document type
|
|
69
71
|
* @template TEntity - Domain entity type
|
|
70
72
|
*
|
|
@@ -85,6 +87,10 @@ export function useFirestoreDocumentRealTime<TDocument, TEntity>(
|
|
|
85
87
|
const [isLoading, setIsLoading] = useState(true);
|
|
86
88
|
const [error, setError] = useState<Error | null>(null);
|
|
87
89
|
|
|
90
|
+
// Stabilize mapper to prevent re-subscriptions when parent re-renders
|
|
91
|
+
const mapperRef = useRef(mapper);
|
|
92
|
+
mapperRef.current = mapper;
|
|
93
|
+
|
|
88
94
|
useEffect(() => {
|
|
89
95
|
// Reset state when userId changes
|
|
90
96
|
if (!userId) {
|
|
@@ -106,7 +112,7 @@ export function useFirestoreDocumentRealTime<TDocument, TEntity>(
|
|
|
106
112
|
docRef,
|
|
107
113
|
(snapshot) => {
|
|
108
114
|
if (snapshot.exists()) {
|
|
109
|
-
const entity =
|
|
115
|
+
const entity = mapperRef.current(snapshot.data() as TDocument, snapshot.id);
|
|
110
116
|
setData(entity);
|
|
111
117
|
} else {
|
|
112
118
|
setData(null);
|
|
@@ -123,7 +129,7 @@ export function useFirestoreDocumentRealTime<TDocument, TEntity>(
|
|
|
123
129
|
return () => {
|
|
124
130
|
unsubscribe();
|
|
125
131
|
};
|
|
126
|
-
}, [userId, docRef,
|
|
132
|
+
}, [userId, docRef, tag]); // Removed mapper from deps
|
|
127
133
|
|
|
128
134
|
const refetch = useCallback(() => {
|
|
129
135
|
// Real-time sync doesn't need refetch, but keep for API compatibility
|
|
@@ -143,6 +149,8 @@ export function useFirestoreDocumentRealTime<TDocument, TEntity>(
|
|
|
143
149
|
/**
|
|
144
150
|
* Generic hook for real-time collection sync via Firestore onSnapshot.
|
|
145
151
|
*
|
|
152
|
+
* PERFORMANCE: Uses useRef to stabilize mapper and optimize snapshot processing
|
|
153
|
+
*
|
|
146
154
|
* @template TDocument - Firestore document type
|
|
147
155
|
* @template TEntity - Domain entity type
|
|
148
156
|
*
|
|
@@ -163,6 +171,10 @@ export function useFirestoreCollectionRealTime<TDocument, TEntity>(
|
|
|
163
171
|
const [isLoading, setIsLoading] = useState(true);
|
|
164
172
|
const [error, setError] = useState<Error | null>(null);
|
|
165
173
|
|
|
174
|
+
// Stabilize mapper to prevent re-subscriptions when parent re-renders
|
|
175
|
+
const mapperRef = useRef(mapper);
|
|
176
|
+
mapperRef.current = mapper;
|
|
177
|
+
|
|
166
178
|
useEffect(() => {
|
|
167
179
|
// Reset state when userId changes
|
|
168
180
|
if (!userId) {
|
|
@@ -178,10 +190,14 @@ export function useFirestoreCollectionRealTime<TDocument, TEntity>(
|
|
|
178
190
|
const unsubscribe = onSnapshot(
|
|
179
191
|
query,
|
|
180
192
|
(snapshot) => {
|
|
181
|
-
|
|
193
|
+
// PERFORMANCE: Pre-allocate array with known size for better memory efficiency
|
|
194
|
+
const entities: TEntity[] = new Array(snapshot.size);
|
|
195
|
+
let index = 0;
|
|
196
|
+
|
|
182
197
|
snapshot.forEach((doc) => {
|
|
183
|
-
entities.
|
|
198
|
+
entities[index++] = mapperRef.current(doc.data() as TDocument, doc.id);
|
|
184
199
|
});
|
|
200
|
+
|
|
185
201
|
setData(entities);
|
|
186
202
|
setIsLoading(false);
|
|
187
203
|
},
|
|
@@ -195,7 +211,7 @@ export function useFirestoreCollectionRealTime<TDocument, TEntity>(
|
|
|
195
211
|
return () => {
|
|
196
212
|
unsubscribe();
|
|
197
213
|
};
|
|
198
|
-
}, [userId, query,
|
|
214
|
+
}, [userId, query, tag]); // Removed mapper from deps
|
|
199
215
|
|
|
200
216
|
const refetch = useCallback(() => {
|
|
201
217
|
// Real-time sync doesn't need refetch, but keep for API compatibility
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Assertion Functions
|
|
3
|
+
*
|
|
4
|
+
* Runtime validation and type narrowing utilities.
|
|
5
|
+
* Throws errors with consistent formatting.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createError } from "./errorConversion";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Assert that a condition is true, throw error otherwise.
|
|
12
|
+
* Useful for validation and runtime checks.
|
|
13
|
+
*/
|
|
14
|
+
export function assert(
|
|
15
|
+
condition: boolean,
|
|
16
|
+
message: string,
|
|
17
|
+
code: string = "ASSERTION_ERROR"
|
|
18
|
+
): asserts condition {
|
|
19
|
+
if (!condition) {
|
|
20
|
+
throw createError(message, code);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Assert that a value is not null/undefined.
|
|
26
|
+
* Throws error if value is null/undefined, returns value otherwise.
|
|
27
|
+
* Useful for type narrowing.
|
|
28
|
+
*/
|
|
29
|
+
export function assertNotNil<T>(
|
|
30
|
+
value: T | null | undefined,
|
|
31
|
+
message: string = "Value should not be null or undefined"
|
|
32
|
+
): T {
|
|
33
|
+
assert(value !== null && value !== undefined, message, "NOT_NIL_ERROR");
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Conversion Utilities
|
|
3
|
+
*
|
|
4
|
+
* Functions to normalize and convert unknown errors to Error objects.
|
|
5
|
+
* Separated for better modularity and testability.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { logError } from "../logger";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Safely convert unknown error to Error object.
|
|
12
|
+
* Useful when catching errors from external APIs.
|
|
13
|
+
*/
|
|
14
|
+
export function toError(error: unknown): Error {
|
|
15
|
+
if (error instanceof Error) {
|
|
16
|
+
return error;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof error === "string") {
|
|
20
|
+
return new Error(error);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (error === null || error === undefined) {
|
|
24
|
+
return new Error("Unknown error occurred");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
return new Error(JSON.stringify(error));
|
|
29
|
+
} catch {
|
|
30
|
+
return new Error(String(error));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create an error with a code and optional cause.
|
|
36
|
+
*/
|
|
37
|
+
export function createError(
|
|
38
|
+
message: string,
|
|
39
|
+
code: string,
|
|
40
|
+
cause?: Error
|
|
41
|
+
): Error {
|
|
42
|
+
const error = new Error(message);
|
|
43
|
+
error.name = code;
|
|
44
|
+
|
|
45
|
+
if (cause) {
|
|
46
|
+
// @ts-ignore - adding cause property
|
|
47
|
+
error.cause = cause;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return error;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Log an error with consistent formatting.
|
|
55
|
+
* Only logs in __DEV__ mode.
|
|
56
|
+
*/
|
|
57
|
+
export function logAndReturnError(
|
|
58
|
+
tag: string,
|
|
59
|
+
message: string,
|
|
60
|
+
error: unknown,
|
|
61
|
+
context?: Record<string, unknown>
|
|
62
|
+
): Error {
|
|
63
|
+
const normalizedError = toError(error);
|
|
64
|
+
|
|
65
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
66
|
+
logError(tag, message, normalizedError, {
|
|
67
|
+
...context,
|
|
68
|
+
originalError: error,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return normalizedError;
|
|
73
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Type Guards
|
|
3
|
+
*
|
|
4
|
+
* Type-safe error checking utilities.
|
|
5
|
+
* Provides type narrowing for error objects.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Type guard to check if value is an Error.
|
|
10
|
+
*/
|
|
11
|
+
export function isError(value: unknown): value is Error {
|
|
12
|
+
return value instanceof Error;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Type guard to check if error has a code property.
|
|
17
|
+
*/
|
|
18
|
+
export function isErrorWithCode(error: Error): error is Error & { code: string } {
|
|
19
|
+
return "code" in error && typeof error.code === "string";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Type guard to check if error has a cause property.
|
|
24
|
+
*/
|
|
25
|
+
export function isErrorWithCause(error: Error): error is Error & { cause: Error } {
|
|
26
|
+
return "cause" in error && error.cause instanceof Error;
|
|
27
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Wrapper Functions
|
|
3
|
+
*
|
|
4
|
+
* Wrap functions with error handling and return Result types.
|
|
5
|
+
* Provides consistent error handling patterns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { logAndReturnError } from "./errorConversion";
|
|
9
|
+
|
|
10
|
+
type Result<T> =
|
|
11
|
+
| { success: true; data: T }
|
|
12
|
+
| { success: false; error: Error };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Wrap an async function with error handling.
|
|
16
|
+
* Returns a Result type with success/error states.
|
|
17
|
+
*/
|
|
18
|
+
export async function tryAsync<T>(
|
|
19
|
+
fn: () => Promise<T>,
|
|
20
|
+
context: { tag: string; operation: string }
|
|
21
|
+
): Promise<Result<T>> {
|
|
22
|
+
try {
|
|
23
|
+
const data = await fn();
|
|
24
|
+
return { success: true, data };
|
|
25
|
+
} catch (error) {
|
|
26
|
+
const normalizedError = logAndReturnError(
|
|
27
|
+
context.tag,
|
|
28
|
+
`Failed to ${context.operation}`,
|
|
29
|
+
error
|
|
30
|
+
);
|
|
31
|
+
return { success: false, error: normalizedError };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Wrap a synchronous function with error handling.
|
|
37
|
+
* Returns a Result type with success/error states.
|
|
38
|
+
*/
|
|
39
|
+
export function trySync<T>(
|
|
40
|
+
fn: () => T,
|
|
41
|
+
context: { tag: string; operation: string }
|
|
42
|
+
): Result<T> {
|
|
43
|
+
try {
|
|
44
|
+
const data = fn();
|
|
45
|
+
return { success: true, data };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const normalizedError = logAndReturnError(
|
|
48
|
+
context.tag,
|
|
49
|
+
`Failed to ${context.operation}`,
|
|
50
|
+
error
|
|
51
|
+
);
|
|
52
|
+
return { success: false, error: normalizedError };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Utilities Module
|
|
3
|
+
*
|
|
4
|
+
* Centralized error handling utilities split into focused modules.
|
|
5
|
+
* Original 195-line file split into 5 files for better maintainability.
|
|
6
|
+
*
|
|
7
|
+
* Modules:
|
|
8
|
+
* - errorConversion: Error normalization and creation
|
|
9
|
+
* - errorTypeGuards: Type-safe error checking
|
|
10
|
+
* - errorWrappers: Function wrapping with error handling
|
|
11
|
+
* - errorAssertions: Runtime validation and type narrowing
|
|
12
|
+
* - serviceErrors: Service-specific error creation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export * from './errorConversion';
|
|
16
|
+
export * from './errorTypeGuards';
|
|
17
|
+
export * from './errorWrappers';
|
|
18
|
+
export * from './errorAssertions';
|
|
19
|
+
export * from './serviceErrors';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service-Specific Error Creators
|
|
3
|
+
*
|
|
4
|
+
* Consistent error creation for external services.
|
|
5
|
+
* Provides clear error codes and messages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createError, toError } from "./errorConversion";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a Firestore-specific error with consistent formatting.
|
|
12
|
+
*/
|
|
13
|
+
export function createFirestoreError(
|
|
14
|
+
operation: string,
|
|
15
|
+
error: unknown
|
|
16
|
+
): Error {
|
|
17
|
+
return createError(
|
|
18
|
+
`Firestore ${operation} failed`,
|
|
19
|
+
"FIRESTORE_ERROR",
|
|
20
|
+
toError(error)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a RevenueCat-specific error with consistent formatting.
|
|
26
|
+
*/
|
|
27
|
+
export function createRevenueCatError(
|
|
28
|
+
operation: string,
|
|
29
|
+
error: unknown
|
|
30
|
+
): Error {
|
|
31
|
+
return createError(
|
|
32
|
+
`RevenueCat ${operation} failed`,
|
|
33
|
+
"REVENUECAT_ERROR",
|
|
34
|
+
toError(error)
|
|
35
|
+
);
|
|
36
|
+
}
|