@umituz/react-native-subscription 3.1.9 → 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 +31 -73
- 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/flowInitialState.ts +22 -0
- package/src/domains/subscription/presentation/flowTypes.ts +106 -0
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx +119 -103
- package/src/domains/subscription/presentation/usePremiumActions.ts +5 -6
- package/src/domains/subscription/presentation/useSubscriptionFlow.ts +25 -92
- package/src/domains/wallet/presentation/components/BalanceCard.tsx +7 -0
- package/src/domains/wallet/presentation/components/TransactionItem.tsx +11 -0
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +34 -60
- package/src/index.components.ts +1 -1
- package/src/shared/infrastructure/SubscriptionEventBus.ts +4 -2
- package/src/shared/presentation/hooks/useFirestoreRealTime.ts +230 -0
- package/src/shared/presentation/types/hookState.types.ts +97 -0
- 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/shared/utils/logger.ts +140 -0
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.states.tsx +0 -187
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
|
+
};
|
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
useRestorePurchase,
|
|
6
6
|
} from '../infrastructure/hooks/useSubscriptionQueries';
|
|
7
7
|
import { usePaywallVisibility } from './usePaywallVisibility';
|
|
8
|
+
import { createLogger } from '../../../shared/utils/logger';
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('usePremiumActions');
|
|
8
11
|
|
|
9
12
|
export interface PremiumActions {
|
|
10
13
|
purchasePackage: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
@@ -38,9 +41,7 @@ export function usePremiumActions(): PremiumActions {
|
|
|
38
41
|
const result = await purchaseMutation.mutateAsync(pkg);
|
|
39
42
|
return result.success;
|
|
40
43
|
} catch (error) {
|
|
41
|
-
|
|
42
|
-
console.error('[usePremiumActions] Purchase failed:', error);
|
|
43
|
-
}
|
|
44
|
+
logger.error('Purchase failed', error, { packageId: pkg.identifier });
|
|
44
45
|
return false;
|
|
45
46
|
}
|
|
46
47
|
},
|
|
@@ -52,9 +53,7 @@ export function usePremiumActions(): PremiumActions {
|
|
|
52
53
|
const result = await restoreMutation.mutateAsync();
|
|
53
54
|
return result.success;
|
|
54
55
|
} catch (error) {
|
|
55
|
-
|
|
56
|
-
console.error('[usePremiumActions] Restore failed:', error);
|
|
57
|
-
}
|
|
56
|
+
logger.error('Restore failed', error);
|
|
58
57
|
return false;
|
|
59
58
|
}
|
|
60
59
|
}, [restoreMutation]);
|
|
@@ -3,109 +3,35 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Single source of truth for app flow state.
|
|
5
5
|
* Clean state transitions without complex if/else logic.
|
|
6
|
+
*
|
|
7
|
+
* State transition rules:
|
|
8
|
+
* - INITIALIZING -> ONBOARDING (first launch)
|
|
9
|
+
* - INITIALIZING -> CHECK_PREMIUM (onboarding already done)
|
|
10
|
+
* - ONBOARDING -> CHECK_PREMIUM (onboarding completed)
|
|
11
|
+
* - CHECK_PREMIUM -> READY (user is premium)
|
|
12
|
+
* - CHECK_PREMIUM -> POST_ONBOARDING_PAYWALL (user not premium, paywall not shown)
|
|
13
|
+
* - CHECK_PREMIUM -> READY (user not premium but paywall already shown)
|
|
14
|
+
* - POST_ONBOARDING_PAYWALL -> READY (paywall closed)
|
|
15
|
+
* - READY -> READY (stays ready, shows overlays when needed)
|
|
6
16
|
*/
|
|
7
17
|
|
|
8
18
|
import { createStore } from "@umituz/react-native-design-system/storage";
|
|
9
19
|
import { subscriptionEventBus, FLOW_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
20
|
+
import {
|
|
21
|
+
SubscriptionFlowStatus,
|
|
22
|
+
SyncStatus,
|
|
23
|
+
type SubscriptionFlowState,
|
|
24
|
+
type SubscriptionFlowActions,
|
|
25
|
+
} from "./flowTypes";
|
|
26
|
+
import { initialFlowState } from "./flowInitialState";
|
|
10
27
|
|
|
11
|
-
export enum SubscriptionFlowStatus {
|
|
12
|
-
INITIALIZING = "INITIALIZING",
|
|
13
|
-
ONBOARDING = "ONBOARDING",
|
|
14
|
-
CHECK_PREMIUM = "CHECK_PREMIUM",
|
|
15
|
-
POST_ONBOARDING_PAYWALL = "POST_ONBOARDING_PAYWALL",
|
|
16
|
-
READY = "READY",
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export enum SyncStatus {
|
|
20
|
-
IDLE = "IDLE",
|
|
21
|
-
SYNCING = "SYNCING",
|
|
22
|
-
SUCCESS = "SUCCESS",
|
|
23
|
-
ERROR = "ERROR",
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface SubscriptionFlowState {
|
|
27
|
-
// Flow state
|
|
28
|
-
status: SubscriptionFlowStatus;
|
|
29
|
-
|
|
30
|
-
// Sync state
|
|
31
|
-
syncStatus: SyncStatus;
|
|
32
|
-
syncError: string | null;
|
|
33
|
-
|
|
34
|
-
// Onboarding state
|
|
35
|
-
isOnboardingComplete: boolean;
|
|
36
|
-
|
|
37
|
-
// Paywall state
|
|
38
|
-
paywallShown: boolean;
|
|
39
|
-
|
|
40
|
-
// Feedback state
|
|
41
|
-
showFeedback: boolean;
|
|
42
|
-
|
|
43
|
-
// Auth modal state
|
|
44
|
-
isAuthModalOpen: boolean;
|
|
45
|
-
|
|
46
|
-
// Initialization flag
|
|
47
|
-
isInitialized: boolean;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface SubscriptionFlowActions {
|
|
51
|
-
// Flow actions
|
|
52
|
-
completeOnboarding: () => void;
|
|
53
|
-
showPaywall: () => void;
|
|
54
|
-
completePaywall: (purchased: boolean) => void;
|
|
55
|
-
showFeedbackScreen: () => void;
|
|
56
|
-
hideFeedback: () => void;
|
|
57
|
-
|
|
58
|
-
// Auth actions
|
|
59
|
-
setAuthModalOpen: (open: boolean) => void;
|
|
60
|
-
|
|
61
|
-
// Sync actions
|
|
62
|
-
setSyncStatus: (status: SyncStatus, error?: string | null) => void;
|
|
63
|
-
|
|
64
|
-
// State setters (for internal use)
|
|
65
|
-
setInitialized: (initialized: boolean) => void;
|
|
66
|
-
setStatus: (status: SubscriptionFlowStatus) => void;
|
|
67
|
-
|
|
68
|
-
// Reset
|
|
69
|
-
resetFlow: () => void;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export type SubscriptionFlowStore = SubscriptionFlowState & SubscriptionFlowActions;
|
|
73
|
-
|
|
74
|
-
const initialState: SubscriptionFlowState = {
|
|
75
|
-
status: SubscriptionFlowStatus.INITIALIZING,
|
|
76
|
-
syncStatus: SyncStatus.IDLE,
|
|
77
|
-
syncError: null,
|
|
78
|
-
isOnboardingComplete: false,
|
|
79
|
-
paywallShown: false,
|
|
80
|
-
showFeedback: false,
|
|
81
|
-
isAuthModalOpen: false,
|
|
82
|
-
isInitialized: false,
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* State transition rules:
|
|
87
|
-
*
|
|
88
|
-
* INITIALIZING -> ONBOARDING (first launch)
|
|
89
|
-
* INITIALIZING -> CHECK_PREMIUM (onboarding already done)
|
|
90
|
-
*
|
|
91
|
-
* ONBOARDING -> CHECK_PREMIUM (onboarding completed)
|
|
92
|
-
*
|
|
93
|
-
* CHECK_PREMIUM -> READY (user is premium)
|
|
94
|
-
* CHECK_PREMIUM -> POST_ONBOARDING_PAYWALL (user not premium, paywall not shown)
|
|
95
|
-
* CHECK_PREMIUM -> READY (user not premium but paywall already shown)
|
|
96
|
-
*
|
|
97
|
-
* POST_ONBOARDING_PAYWALL -> READY (paywall closed)
|
|
98
|
-
*
|
|
99
|
-
* READY -> READY (stays ready, shows overlays when needed)
|
|
100
|
-
*/
|
|
101
28
|
export const useSubscriptionFlowStore = createStore<SubscriptionFlowState, SubscriptionFlowActions>({
|
|
102
29
|
name: "subscription-flow-storage",
|
|
103
|
-
initialState,
|
|
30
|
+
initialState: initialFlowState,
|
|
104
31
|
persist: true,
|
|
105
32
|
onRehydrate: (state) => {
|
|
106
33
|
if (!state.isInitialized) {
|
|
107
34
|
state.setInitialized(true);
|
|
108
|
-
|
|
109
35
|
// First time: show onboarding
|
|
110
36
|
state.setStatus(SubscriptionFlowStatus.INITIALIZING);
|
|
111
37
|
} else if (state.isOnboardingComplete) {
|
|
@@ -176,3 +102,10 @@ export const useSubscriptionFlowStore = createStore<SubscriptionFlowState, Subsc
|
|
|
176
102
|
},
|
|
177
103
|
}),
|
|
178
104
|
});
|
|
105
|
+
|
|
106
|
+
// Re-export types for convenience
|
|
107
|
+
export type { SubscriptionFlowState, SubscriptionFlowActions } from "./flowTypes";
|
|
108
|
+
export { SubscriptionFlowStatus, SyncStatus } from "./flowTypes";
|
|
109
|
+
|
|
110
|
+
// Re-export store type inferred from createStore
|
|
111
|
+
export type SubscriptionFlowStore = ReturnType<typeof useSubscriptionFlowStore>;
|
|
@@ -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
|
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo } from "react";
|
|
2
2
|
import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
|
|
3
|
-
import { collection,
|
|
3
|
+
import { collection, query, orderBy, limit } from "firebase/firestore";
|
|
4
4
|
import type {
|
|
5
5
|
CreditLog,
|
|
6
6
|
TransactionRepositoryConfig,
|
|
7
7
|
} from "../../domain/types/transaction.types";
|
|
8
8
|
import { requireFirestore } from "../../../../shared/infrastructure/firestore/collectionUtils";
|
|
9
|
+
import { useFirestoreCollectionRealTime } from "../../../../shared/presentation/hooks/useFirestoreRealTime";
|
|
9
10
|
|
|
10
11
|
export interface UseTransactionHistoryParams {
|
|
11
12
|
config: TransactionRepositoryConfig;
|
|
@@ -20,78 +21,51 @@ interface UseTransactionHistoryResult {
|
|
|
20
21
|
isEmpty: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Mapper to convert Firestore document to CreditLog entity.
|
|
26
|
+
*/
|
|
27
|
+
function mapTransactionLog(doc: any, docId: string): CreditLog {
|
|
28
|
+
return {
|
|
29
|
+
id: docId,
|
|
30
|
+
...doc,
|
|
31
|
+
} as CreditLog;
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
export function useTransactionHistory({
|
|
24
35
|
config,
|
|
25
36
|
limit: limitCount = 50,
|
|
26
37
|
}: UseTransactionHistoryParams): UseTransactionHistoryResult {
|
|
27
38
|
const userId = useAuthStore(selectUserId);
|
|
28
|
-
const [transactions, setTransactions] = useState<CreditLog[]>([]);
|
|
29
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
30
|
-
const [error, setError] = useState<Error | null>(null);
|
|
31
|
-
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
if (!userId) {
|
|
34
|
-
setTransactions([]);
|
|
35
|
-
setIsLoading(false);
|
|
36
|
-
setError(null);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// Build collection query
|
|
41
|
+
const queryRef = useMemo(() => {
|
|
42
|
+
if (!userId) return null;
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
: config.collectionName;
|
|
44
|
+
const db = requireFirestore();
|
|
45
|
+
const collectionPath = config.useUserSubcollection
|
|
46
|
+
? `users/${userId}/${config.collectionName}`
|
|
47
|
+
: config.collectionName;
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const unsubscribe = onSnapshot(
|
|
56
|
-
q,
|
|
57
|
-
(snapshot) => {
|
|
58
|
-
const logs: CreditLog[] = [];
|
|
59
|
-
snapshot.forEach((doc) => {
|
|
60
|
-
logs.push({
|
|
61
|
-
id: doc.id,
|
|
62
|
-
...doc.data(),
|
|
63
|
-
} as CreditLog);
|
|
64
|
-
});
|
|
65
|
-
setTransactions(logs);
|
|
66
|
-
setIsLoading(false);
|
|
67
|
-
},
|
|
68
|
-
(err) => {
|
|
69
|
-
console.error("[useTransactionHistory] Snapshot error:", err);
|
|
70
|
-
setError(err as Error);
|
|
71
|
-
setIsLoading(false);
|
|
72
|
-
}
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
return () => unsubscribe();
|
|
76
|
-
} catch (err) {
|
|
77
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
78
|
-
console.error("[useTransactionHistory] Setup error:", err);
|
|
79
|
-
setError(error);
|
|
80
|
-
setIsLoading(false);
|
|
81
|
-
}
|
|
49
|
+
return query(
|
|
50
|
+
collection(db, collectionPath),
|
|
51
|
+
orderBy("timestamp", "desc"),
|
|
52
|
+
limit(limitCount)
|
|
53
|
+
);
|
|
82
54
|
}, [userId, config.collectionName, config.useUserSubcollection, limitCount]);
|
|
83
55
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
56
|
+
// Use generic real-time sync hook
|
|
57
|
+
const { data, isLoading, error, refetch, isEmpty } = useFirestoreCollectionRealTime(
|
|
58
|
+
userId,
|
|
59
|
+
queryRef,
|
|
60
|
+
mapTransactionLog,
|
|
61
|
+
"useTransactionHistory"
|
|
62
|
+
);
|
|
89
63
|
|
|
90
64
|
return {
|
|
91
|
-
transactions,
|
|
65
|
+
transactions: data,
|
|
92
66
|
isLoading,
|
|
93
67
|
error,
|
|
94
68
|
refetch,
|
|
95
|
-
isEmpty
|
|
69
|
+
isEmpty,
|
|
96
70
|
};
|
|
97
71
|
}
|
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) {
|