@umituz/react-native-subscription 2.27.144 → 2.27.146
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/subscription/presentation/featureGateActions.ts +37 -0
- package/src/domains/subscription/presentation/featureGateHelpers.ts +31 -0
- package/src/domains/subscription/presentation/featureGateRefs.ts +27 -0
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.constants.ts +1 -0
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.styles.ts +41 -0
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.tsx +18 -108
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.types.ts +20 -0
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx +58 -0
- package/src/domains/subscription/presentation/useFeatureGate.ts +46 -94
- package/src/domains/subscription/presentation/useFeatureGate.types.ts +18 -0
- package/src/domains/wallet/presentation/components/TransactionItem.styles.ts +30 -0
- package/src/domains/wallet/presentation/components/TransactionItem.tsx +16 -100
- package/src/domains/wallet/presentation/components/TransactionItem.types.ts +18 -0
- package/src/domains/wallet/presentation/components/TransactionList.constants.ts +1 -0
- package/src/domains/wallet/presentation/components/TransactionList.styles.ts +34 -0
- package/src/domains/wallet/presentation/components/TransactionList.tsx +15 -81
- package/src/domains/wallet/presentation/components/TransactionListStates.tsx +28 -0
- package/src/domains/wallet/presentation/components/transactionItemHelpers.ts +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.27.
|
|
3
|
+
"version": "2.27.146",
|
|
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",
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { MutableRefObject } from "react";
|
|
2
|
+
|
|
3
|
+
export const executeFeatureAction = (
|
|
4
|
+
action: () => void | Promise<void>,
|
|
5
|
+
isAuthenticated: boolean,
|
|
6
|
+
onShowAuthModal: (callback: () => void | Promise<void>) => void,
|
|
7
|
+
hasSubscriptionRef: MutableRefObject<boolean>,
|
|
8
|
+
creditBalanceRef: MutableRefObject<number>,
|
|
9
|
+
requiredCreditsRef: MutableRefObject<number>,
|
|
10
|
+
onShowPaywallRef: MutableRefObject<(requiredCredits?: number) => void>,
|
|
11
|
+
pendingActionRef: MutableRefObject<(() => void | Promise<void>) | null>,
|
|
12
|
+
isWaitingForAuthCreditsRef: MutableRefObject<boolean>,
|
|
13
|
+
isWaitingForPurchaseRef: MutableRefObject<boolean>
|
|
14
|
+
): void => {
|
|
15
|
+
if (!isAuthenticated) {
|
|
16
|
+
const postAuthAction = () => {
|
|
17
|
+
pendingActionRef.current = action;
|
|
18
|
+
isWaitingForAuthCreditsRef.current = true;
|
|
19
|
+
};
|
|
20
|
+
onShowAuthModal(postAuthAction);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (hasSubscriptionRef.current) {
|
|
25
|
+
action();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (creditBalanceRef.current < requiredCreditsRef.current) {
|
|
30
|
+
pendingActionRef.current = action;
|
|
31
|
+
isWaitingForPurchaseRef.current = true;
|
|
32
|
+
onShowPaywallRef.current(requiredCreditsRef.current);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
action();
|
|
37
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const DEFAULT_REQUIRED_CREDITS = 1;
|
|
2
|
+
|
|
3
|
+
export const shouldExecuteAuthAction = (
|
|
4
|
+
isWaitingForAuthCredits: boolean,
|
|
5
|
+
isCreditsLoaded: boolean,
|
|
6
|
+
hasPendingAction: boolean,
|
|
7
|
+
hasSubscription: boolean,
|
|
8
|
+
creditBalance: number,
|
|
9
|
+
requiredCredits: number
|
|
10
|
+
): boolean => {
|
|
11
|
+
if (!isWaitingForAuthCredits || !isCreditsLoaded || !hasPendingAction) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return hasSubscription || creditBalance >= requiredCredits;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const shouldExecutePurchaseAction = (
|
|
18
|
+
isWaitingForPurchase: boolean,
|
|
19
|
+
creditBalance: number,
|
|
20
|
+
prevBalance: number,
|
|
21
|
+
hasSubscription: boolean,
|
|
22
|
+
prevHasSubscription: boolean,
|
|
23
|
+
hasPendingAction: boolean
|
|
24
|
+
): boolean => {
|
|
25
|
+
if (!isWaitingForPurchase || !hasPendingAction) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const creditsIncreased = creditBalance > prevBalance;
|
|
29
|
+
const subscriptionAcquired = hasSubscription && !prevHasSubscription;
|
|
30
|
+
return creditsIncreased || subscriptionAcquired;
|
|
31
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useRef, useEffect, type MutableRefObject } from "react";
|
|
2
|
+
|
|
3
|
+
export interface FeatureGateRefs {
|
|
4
|
+
creditBalanceRef: MutableRefObject<number>;
|
|
5
|
+
hasSubscriptionRef: MutableRefObject<boolean>;
|
|
6
|
+
onShowPaywallRef: MutableRefObject<(requiredCredits?: number) => void>;
|
|
7
|
+
requiredCreditsRef: MutableRefObject<number>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const useSyncedRefs = (
|
|
11
|
+
creditBalance: number,
|
|
12
|
+
hasSubscription: boolean,
|
|
13
|
+
onShowPaywall: (requiredCredits?: number) => void,
|
|
14
|
+
requiredCredits: number
|
|
15
|
+
): FeatureGateRefs => {
|
|
16
|
+
const creditBalanceRef = useRef(creditBalance);
|
|
17
|
+
const hasSubscriptionRef = useRef(hasSubscription);
|
|
18
|
+
const onShowPaywallRef = useRef(onShowPaywall);
|
|
19
|
+
const requiredCreditsRef = useRef(requiredCredits);
|
|
20
|
+
|
|
21
|
+
useEffect(() => { creditBalanceRef.current = creditBalance; }, [creditBalance]);
|
|
22
|
+
useEffect(() => { hasSubscriptionRef.current = hasSubscription; }, [hasSubscription]);
|
|
23
|
+
useEffect(() => { onShowPaywallRef.current = onShowPaywall; }, [onShowPaywall]);
|
|
24
|
+
useEffect(() => { requiredCreditsRef.current = requiredCredits; }, [requiredCredits]);
|
|
25
|
+
|
|
26
|
+
return { creditBalanceRef, hasSubscriptionRef, onShowPaywallRef, requiredCreditsRef };
|
|
27
|
+
};
|
package/src/domains/subscription/presentation/screens/components/SubscriptionHeader.constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const EXPIRING_SOON_THRESHOLD_DAYS = 7;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import type { AppDesignTokens } from "@umituz/react-native-design-system";
|
|
3
|
+
|
|
4
|
+
export const createSubscriptionHeaderStyles = (tokens: AppDesignTokens) =>
|
|
5
|
+
StyleSheet.create({
|
|
6
|
+
container: {
|
|
7
|
+
borderRadius: tokens.radius.lg,
|
|
8
|
+
padding: tokens.spacing.lg,
|
|
9
|
+
gap: tokens.spacing.lg,
|
|
10
|
+
backgroundColor: tokens.colors.surface,
|
|
11
|
+
},
|
|
12
|
+
header: {
|
|
13
|
+
flexDirection: "row",
|
|
14
|
+
justifyContent: "space-between",
|
|
15
|
+
alignItems: "center",
|
|
16
|
+
},
|
|
17
|
+
titleContainer: {
|
|
18
|
+
flex: 1,
|
|
19
|
+
marginRight: tokens.spacing.md,
|
|
20
|
+
},
|
|
21
|
+
title: {
|
|
22
|
+
fontWeight: "700",
|
|
23
|
+
},
|
|
24
|
+
details: {
|
|
25
|
+
gap: tokens.spacing.md,
|
|
26
|
+
paddingTop: tokens.spacing.md,
|
|
27
|
+
},
|
|
28
|
+
row: {
|
|
29
|
+
flexDirection: "row",
|
|
30
|
+
justifyContent: "space-between",
|
|
31
|
+
alignItems: "center",
|
|
32
|
+
gap: tokens.spacing.lg,
|
|
33
|
+
},
|
|
34
|
+
label: {
|
|
35
|
+
flex: 1,
|
|
36
|
+
},
|
|
37
|
+
value: {
|
|
38
|
+
fontWeight: "600",
|
|
39
|
+
textAlign: "right",
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -1,34 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscription Header Component
|
|
3
|
-
* Displays status badge and subscription details
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import React, { useMemo } from "react";
|
|
7
|
-
import { View
|
|
2
|
+
import { View } from "react-native";
|
|
8
3
|
import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
9
4
|
import { PremiumStatusBadge } from "../../components/details/PremiumStatusBadge";
|
|
10
|
-
import {
|
|
5
|
+
import type { SubscriptionHeaderProps } from "./SubscriptionHeader.types";
|
|
6
|
+
import { createSubscriptionHeaderStyles } from "./SubscriptionHeader.styles";
|
|
7
|
+
import { EXPIRING_SOON_THRESHOLD_DAYS } from "./SubscriptionHeader.constants";
|
|
8
|
+
import { SubscriptionHeaderContent } from "./SubscriptionHeaderContent";
|
|
11
9
|
|
|
12
|
-
export
|
|
13
|
-
statusType: "active" | "expired" | "none" | "canceled";
|
|
14
|
-
showExpirationDate: boolean;
|
|
15
|
-
isLifetime: boolean;
|
|
16
|
-
expirationDate?: string;
|
|
17
|
-
purchaseDate?: string;
|
|
18
|
-
daysRemaining?: number | null;
|
|
19
|
-
hideTitle?: boolean;
|
|
20
|
-
translations: {
|
|
21
|
-
title: string;
|
|
22
|
-
statusActive: string;
|
|
23
|
-
statusExpired: string;
|
|
24
|
-
statusFree: string;
|
|
25
|
-
statusCanceled: string;
|
|
26
|
-
statusLabel: string;
|
|
27
|
-
lifetimeLabel: string;
|
|
28
|
-
expiresLabel: string;
|
|
29
|
-
purchasedLabel: string;
|
|
30
|
-
};
|
|
31
|
-
}
|
|
10
|
+
export type { SubscriptionHeaderProps } from "./SubscriptionHeader.types";
|
|
32
11
|
|
|
33
12
|
export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
|
|
34
13
|
statusType,
|
|
@@ -41,60 +20,15 @@ export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
|
|
|
41
20
|
translations,
|
|
42
21
|
}) => {
|
|
43
22
|
const tokens = useAppDesignTokens();
|
|
44
|
-
const showExpiring =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const styles = useMemo(
|
|
48
|
-
() =>
|
|
49
|
-
StyleSheet.create({
|
|
50
|
-
container: {
|
|
51
|
-
borderRadius: tokens.radius.lg,
|
|
52
|
-
padding: tokens.spacing.lg,
|
|
53
|
-
gap: tokens.spacing.lg,
|
|
54
|
-
backgroundColor: tokens.colors.surface,
|
|
55
|
-
},
|
|
56
|
-
header: {
|
|
57
|
-
flexDirection: "row",
|
|
58
|
-
justifyContent: "space-between",
|
|
59
|
-
alignItems: "center",
|
|
60
|
-
},
|
|
61
|
-
titleContainer: {
|
|
62
|
-
flex: 1,
|
|
63
|
-
marginRight: tokens.spacing.md,
|
|
64
|
-
},
|
|
65
|
-
title: {
|
|
66
|
-
fontWeight: "700",
|
|
67
|
-
},
|
|
68
|
-
details: {
|
|
69
|
-
gap: tokens.spacing.md,
|
|
70
|
-
paddingTop: tokens.spacing.md,
|
|
71
|
-
},
|
|
72
|
-
row: {
|
|
73
|
-
flexDirection: "row",
|
|
74
|
-
justifyContent: "space-between",
|
|
75
|
-
alignItems: "center",
|
|
76
|
-
gap: tokens.spacing.lg,
|
|
77
|
-
},
|
|
78
|
-
label: {
|
|
79
|
-
flex: 1,
|
|
80
|
-
},
|
|
81
|
-
value: {
|
|
82
|
-
fontWeight: "600",
|
|
83
|
-
textAlign: "right",
|
|
84
|
-
},
|
|
85
|
-
}),
|
|
86
|
-
[tokens]
|
|
87
|
-
);
|
|
23
|
+
const showExpiring = daysRemaining !== null && daysRemaining !== undefined && daysRemaining <= EXPIRING_SOON_THRESHOLD_DAYS;
|
|
24
|
+
const styles = useMemo(() => createSubscriptionHeaderStyles(tokens), [tokens]);
|
|
88
25
|
|
|
89
26
|
return (
|
|
90
27
|
<View style={styles.container}>
|
|
91
28
|
<View style={styles.header}>
|
|
92
29
|
{!hideTitle && (
|
|
93
30
|
<View style={styles.titleContainer}>
|
|
94
|
-
<AtomicText
|
|
95
|
-
type="headlineSmall"
|
|
96
|
-
style={[styles.title, { color: tokens.colors.textPrimary }]}
|
|
97
|
-
>
|
|
31
|
+
<AtomicText type="headlineSmall" style={[styles.title, { color: tokens.colors.textPrimary }]}>
|
|
98
32
|
{translations.title}
|
|
99
33
|
</AtomicText>
|
|
100
34
|
</View>
|
|
@@ -108,39 +42,15 @@ export const SubscriptionHeader: React.FC<SubscriptionHeaderProps> = ({
|
|
|
108
42
|
/>
|
|
109
43
|
</View>
|
|
110
44
|
|
|
111
|
-
<
|
|
112
|
-
{isLifetime
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
) : (
|
|
121
|
-
<>
|
|
122
|
-
{showExpirationDate && expirationDate && (
|
|
123
|
-
<DetailRow
|
|
124
|
-
label={translations.expiresLabel}
|
|
125
|
-
value={expirationDate}
|
|
126
|
-
highlight={showExpiring}
|
|
127
|
-
style={styles.row}
|
|
128
|
-
labelStyle={styles.label}
|
|
129
|
-
valueStyle={styles.value}
|
|
130
|
-
/>
|
|
131
|
-
)}
|
|
132
|
-
{purchaseDate && (
|
|
133
|
-
<DetailRow
|
|
134
|
-
label={translations.purchasedLabel}
|
|
135
|
-
value={purchaseDate}
|
|
136
|
-
style={styles.row}
|
|
137
|
-
labelStyle={styles.label}
|
|
138
|
-
valueStyle={styles.value}
|
|
139
|
-
/>
|
|
140
|
-
)}
|
|
141
|
-
</>
|
|
142
|
-
)}
|
|
143
|
-
</View>
|
|
45
|
+
<SubscriptionHeaderContent
|
|
46
|
+
isLifetime={isLifetime}
|
|
47
|
+
showExpirationDate={showExpirationDate}
|
|
48
|
+
expirationDate={expirationDate}
|
|
49
|
+
purchaseDate={purchaseDate}
|
|
50
|
+
showExpiring={showExpiring}
|
|
51
|
+
translations={translations}
|
|
52
|
+
styles={styles}
|
|
53
|
+
/>
|
|
144
54
|
</View>
|
|
145
55
|
);
|
|
146
56
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface SubscriptionHeaderProps {
|
|
2
|
+
statusType: "active" | "expired" | "none" | "canceled";
|
|
3
|
+
showExpirationDate: boolean;
|
|
4
|
+
isLifetime: boolean;
|
|
5
|
+
expirationDate?: string;
|
|
6
|
+
purchaseDate?: string;
|
|
7
|
+
daysRemaining?: number | null;
|
|
8
|
+
hideTitle?: boolean;
|
|
9
|
+
translations: {
|
|
10
|
+
title: string;
|
|
11
|
+
statusActive: string;
|
|
12
|
+
statusExpired: string;
|
|
13
|
+
statusFree: string;
|
|
14
|
+
statusCanceled: string;
|
|
15
|
+
statusLabel: string;
|
|
16
|
+
lifetimeLabel: string;
|
|
17
|
+
expiresLabel: string;
|
|
18
|
+
purchasedLabel: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { DetailRow } from "../../components/details/DetailRow";
|
|
4
|
+
import type { SubscriptionHeaderProps } from "./SubscriptionHeader.types";
|
|
5
|
+
|
|
6
|
+
interface SubscriptionHeaderContentProps {
|
|
7
|
+
isLifetime: boolean;
|
|
8
|
+
showExpirationDate: boolean;
|
|
9
|
+
expirationDate?: string;
|
|
10
|
+
purchaseDate?: string;
|
|
11
|
+
showExpiring: boolean;
|
|
12
|
+
translations: SubscriptionHeaderProps["translations"];
|
|
13
|
+
styles: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const SubscriptionHeaderContent: React.FC<SubscriptionHeaderContentProps> = ({
|
|
17
|
+
isLifetime,
|
|
18
|
+
showExpirationDate,
|
|
19
|
+
expirationDate,
|
|
20
|
+
purchaseDate,
|
|
21
|
+
showExpiring,
|
|
22
|
+
translations,
|
|
23
|
+
styles,
|
|
24
|
+
}) => (
|
|
25
|
+
<View style={styles.details}>
|
|
26
|
+
{isLifetime ? (
|
|
27
|
+
<DetailRow
|
|
28
|
+
label={translations.statusLabel}
|
|
29
|
+
value={translations.lifetimeLabel}
|
|
30
|
+
style={styles.row}
|
|
31
|
+
labelStyle={styles.label}
|
|
32
|
+
valueStyle={styles.value}
|
|
33
|
+
/>
|
|
34
|
+
) : (
|
|
35
|
+
<>
|
|
36
|
+
{showExpirationDate && expirationDate && (
|
|
37
|
+
<DetailRow
|
|
38
|
+
label={translations.expiresLabel}
|
|
39
|
+
value={expirationDate}
|
|
40
|
+
highlight={showExpiring}
|
|
41
|
+
style={styles.row}
|
|
42
|
+
labelStyle={styles.label}
|
|
43
|
+
valueStyle={styles.value}
|
|
44
|
+
/>
|
|
45
|
+
)}
|
|
46
|
+
{purchaseDate && (
|
|
47
|
+
<DetailRow
|
|
48
|
+
label={translations.purchasedLabel}
|
|
49
|
+
value={purchaseDate}
|
|
50
|
+
style={styles.row}
|
|
51
|
+
labelStyle={styles.label}
|
|
52
|
+
valueStyle={styles.value}
|
|
53
|
+
/>
|
|
54
|
+
)}
|
|
55
|
+
</>
|
|
56
|
+
)}
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
@@ -1,30 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useFeatureGate Hook
|
|
3
|
-
* Unified feature gate: Auth → Subscription → Credits
|
|
4
|
-
* Uses ref pattern to avoid stale closure issues.
|
|
5
|
-
* Event-driven approach - no polling, no waiting.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { useCallback, useRef, useEffect } from "react";
|
|
2
|
+
import type { UseFeatureGateParams, UseFeatureGateResult } from "./useFeatureGate.types";
|
|
3
|
+
import { DEFAULT_REQUIRED_CREDITS, shouldExecuteAuthAction, shouldExecutePurchaseAction } from "./featureGateHelpers";
|
|
4
|
+
import { useSyncedRefs } from "./featureGateRefs";
|
|
5
|
+
import { executeFeatureAction } from "./featureGateActions";
|
|
9
6
|
|
|
10
|
-
export
|
|
11
|
-
readonly isAuthenticated: boolean;
|
|
12
|
-
readonly onShowAuthModal: (pendingCallback: () => void | Promise<void>) => void;
|
|
13
|
-
readonly hasSubscription?: boolean;
|
|
14
|
-
readonly creditBalance: number;
|
|
15
|
-
readonly requiredCredits?: number;
|
|
16
|
-
readonly onShowPaywall: (requiredCredits?: number) => void;
|
|
17
|
-
readonly isCreditsLoaded?: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface UseFeatureGateResult {
|
|
21
|
-
readonly requireFeature: (action: () => void | Promise<void>) => void;
|
|
22
|
-
readonly isAuthenticated: boolean;
|
|
23
|
-
readonly hasSubscription: boolean;
|
|
24
|
-
readonly hasCredits: boolean;
|
|
25
|
-
readonly creditBalance: number;
|
|
26
|
-
readonly canAccess: boolean;
|
|
27
|
-
}
|
|
7
|
+
export type { UseFeatureGateParams, UseFeatureGateResult } from "./useFeatureGate.types";
|
|
28
8
|
|
|
29
9
|
export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResult {
|
|
30
10
|
const {
|
|
@@ -32,7 +12,7 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
32
12
|
onShowAuthModal,
|
|
33
13
|
hasSubscription = false,
|
|
34
14
|
creditBalance,
|
|
35
|
-
requiredCredits =
|
|
15
|
+
requiredCredits = DEFAULT_REQUIRED_CREDITS,
|
|
36
16
|
onShowPaywall,
|
|
37
17
|
isCreditsLoaded = true,
|
|
38
18
|
} = params;
|
|
@@ -42,52 +22,41 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
42
22
|
const isWaitingForPurchaseRef = useRef(false);
|
|
43
23
|
const isWaitingForAuthCreditsRef = useRef(false);
|
|
44
24
|
|
|
45
|
-
const creditBalanceRef =
|
|
46
|
-
const hasSubscriptionRef = useRef(hasSubscription);
|
|
47
|
-
const onShowPaywallRef = useRef(onShowPaywall);
|
|
48
|
-
const requiredCreditsRef = useRef(requiredCredits);
|
|
49
|
-
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
creditBalanceRef.current = creditBalance;
|
|
52
|
-
}, [creditBalance]);
|
|
53
|
-
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
hasSubscriptionRef.current = hasSubscription;
|
|
56
|
-
}, [hasSubscription]);
|
|
57
|
-
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
onShowPaywallRef.current = onShowPaywall;
|
|
60
|
-
}, [onShowPaywall]);
|
|
61
|
-
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
requiredCreditsRef.current = requiredCredits;
|
|
64
|
-
}, [requiredCredits]);
|
|
25
|
+
const { creditBalanceRef, hasSubscriptionRef, onShowPaywallRef, requiredCreditsRef } = useSyncedRefs(creditBalance, hasSubscription, onShowPaywall, requiredCredits);
|
|
65
26
|
|
|
66
27
|
useEffect(() => {
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
28
|
+
if (shouldExecuteAuthAction(
|
|
29
|
+
isWaitingForAuthCreditsRef.current,
|
|
30
|
+
isCreditsLoaded,
|
|
31
|
+
!!pendingActionRef.current,
|
|
32
|
+
hasSubscription,
|
|
33
|
+
creditBalance,
|
|
34
|
+
requiredCredits
|
|
35
|
+
)) {
|
|
36
|
+
isWaitingForAuthCreditsRef.current = false;
|
|
37
|
+
const action = pendingActionRef.current!;
|
|
75
38
|
pendingActionRef.current = null;
|
|
76
39
|
action();
|
|
77
40
|
return;
|
|
78
41
|
}
|
|
79
42
|
|
|
80
|
-
|
|
81
|
-
|
|
43
|
+
if (isWaitingForAuthCreditsRef.current && isCreditsLoaded && pendingActionRef.current) {
|
|
44
|
+
isWaitingForAuthCreditsRef.current = false;
|
|
45
|
+
isWaitingForPurchaseRef.current = true;
|
|
46
|
+
onShowPaywall(requiredCredits);
|
|
47
|
+
}
|
|
82
48
|
}, [isCreditsLoaded, creditBalance, hasSubscription, requiredCredits, onShowPaywall]);
|
|
83
49
|
|
|
84
50
|
useEffect(() => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
51
|
+
if (shouldExecutePurchaseAction(
|
|
52
|
+
isWaitingForPurchaseRef.current,
|
|
53
|
+
creditBalance,
|
|
54
|
+
prevCreditBalanceRef.current ?? 0,
|
|
55
|
+
hasSubscription,
|
|
56
|
+
hasSubscriptionRef.current,
|
|
57
|
+
!!pendingActionRef.current
|
|
58
|
+
)) {
|
|
59
|
+
const action = pendingActionRef.current!;
|
|
91
60
|
pendingActionRef.current = null;
|
|
92
61
|
isWaitingForPurchaseRef.current = false;
|
|
93
62
|
action();
|
|
@@ -99,45 +68,28 @@ export function useFeatureGate(params: UseFeatureGateParams): UseFeatureGateResu
|
|
|
99
68
|
|
|
100
69
|
const requireFeature = useCallback(
|
|
101
70
|
(action: () => void | Promise<void>) => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const currentRequiredCredits = requiredCreditsRef.current;
|
|
115
|
-
|
|
116
|
-
if (currentHasSubscription) {
|
|
117
|
-
action();
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (currentBalance < currentRequiredCredits) {
|
|
122
|
-
pendingActionRef.current = action;
|
|
123
|
-
isWaitingForPurchaseRef.current = true;
|
|
124
|
-
onShowPaywallRef.current(currentRequiredCredits);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
action();
|
|
71
|
+
executeFeatureAction(
|
|
72
|
+
action,
|
|
73
|
+
isAuthenticated,
|
|
74
|
+
onShowAuthModal,
|
|
75
|
+
hasSubscriptionRef,
|
|
76
|
+
creditBalanceRef,
|
|
77
|
+
requiredCreditsRef,
|
|
78
|
+
onShowPaywallRef,
|
|
79
|
+
pendingActionRef,
|
|
80
|
+
isWaitingForAuthCreditsRef,
|
|
81
|
+
isWaitingForPurchaseRef
|
|
82
|
+
);
|
|
129
83
|
},
|
|
130
|
-
[isAuthenticated, onShowAuthModal]
|
|
84
|
+
[isAuthenticated, onShowAuthModal, hasSubscriptionRef, creditBalanceRef, requiredCreditsRef, onShowPaywallRef]
|
|
131
85
|
);
|
|
132
86
|
|
|
133
|
-
const hasCredits = creditBalance >= requiredCredits;
|
|
134
|
-
|
|
135
87
|
return {
|
|
136
88
|
requireFeature,
|
|
137
89
|
isAuthenticated,
|
|
138
90
|
hasSubscription,
|
|
139
|
-
hasCredits,
|
|
91
|
+
hasCredits: creditBalance >= requiredCredits,
|
|
140
92
|
creditBalance,
|
|
141
|
-
canAccess: isAuthenticated && (hasSubscription ||
|
|
93
|
+
canAccess: isAuthenticated && (hasSubscription || creditBalance >= requiredCredits),
|
|
142
94
|
};
|
|
143
95
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface UseFeatureGateParams {
|
|
2
|
+
readonly isAuthenticated: boolean;
|
|
3
|
+
readonly onShowAuthModal: (pendingCallback: () => void | Promise<void>) => void;
|
|
4
|
+
readonly hasSubscription?: boolean;
|
|
5
|
+
readonly creditBalance: number;
|
|
6
|
+
readonly requiredCredits?: number;
|
|
7
|
+
readonly onShowPaywall: (requiredCredits?: number) => void;
|
|
8
|
+
readonly isCreditsLoaded?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UseFeatureGateResult {
|
|
12
|
+
readonly requireFeature: (action: () => void | Promise<void>) => void;
|
|
13
|
+
readonly isAuthenticated: boolean;
|
|
14
|
+
readonly hasSubscription: boolean;
|
|
15
|
+
readonly hasCredits: boolean;
|
|
16
|
+
readonly creditBalance: number;
|
|
17
|
+
readonly canAccess: boolean;
|
|
18
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
|
|
3
|
+
export const transactionItemStyles = StyleSheet.create({
|
|
4
|
+
container: {
|
|
5
|
+
flexDirection: "row",
|
|
6
|
+
alignItems: "center",
|
|
7
|
+
padding: 12,
|
|
8
|
+
borderRadius: 12,
|
|
9
|
+
marginBottom: 8,
|
|
10
|
+
},
|
|
11
|
+
iconContainer: {
|
|
12
|
+
width: 40,
|
|
13
|
+
height: 40,
|
|
14
|
+
borderRadius: 20,
|
|
15
|
+
justifyContent: "center",
|
|
16
|
+
alignItems: "center",
|
|
17
|
+
marginRight: 12,
|
|
18
|
+
},
|
|
19
|
+
content: {
|
|
20
|
+
flex: 1,
|
|
21
|
+
gap: 2,
|
|
22
|
+
},
|
|
23
|
+
reason: {
|
|
24
|
+
fontWeight: "600",
|
|
25
|
+
},
|
|
26
|
+
change: {
|
|
27
|
+
fontWeight: "700",
|
|
28
|
+
marginLeft: 12,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -1,41 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transaction Item Component
|
|
3
|
-
*
|
|
4
|
-
* Displays a single credit transaction.
|
|
5
|
-
* Props-driven for full customization.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import React, { useMemo } from "react";
|
|
9
|
-
import { View
|
|
10
|
-
import {
|
|
11
|
-
useAppDesignTokens,
|
|
12
|
-
AtomicText,
|
|
13
|
-
AtomicIcon,
|
|
14
|
-
} from "@umituz/react-native-design-system";
|
|
15
|
-
import { timezoneService } from "@umituz/react-native-design-system";
|
|
16
|
-
import type { CreditLog } from "../../domain/types/transaction.types";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { useAppDesignTokens, AtomicText, AtomicIcon } from "@umituz/react-native-design-system";
|
|
17
4
|
import { getTransactionIcon } from "../../utils/transactionIconMap";
|
|
5
|
+
import { transactionItemStyles } from "./TransactionItem.styles";
|
|
6
|
+
import type { TransactionItemProps } from "./TransactionItem.types";
|
|
7
|
+
import { defaultDateFormatter, getReasonLabel, getChangePrefix } from "./transactionItemHelpers";
|
|
18
8
|
|
|
19
|
-
export
|
|
20
|
-
purchase: string;
|
|
21
|
-
usage: string;
|
|
22
|
-
refund: string;
|
|
23
|
-
bonus: string;
|
|
24
|
-
subscription: string;
|
|
25
|
-
admin: string;
|
|
26
|
-
reward: string;
|
|
27
|
-
expired: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface TransactionItemProps {
|
|
31
|
-
transaction: CreditLog;
|
|
32
|
-
translations: TransactionItemTranslations;
|
|
33
|
-
dateFormatter?: (timestamp: number) => string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const defaultDateFormatter = (timestamp: number): string => {
|
|
37
|
-
return timezoneService.formatToDisplayDateTime(new Date(timestamp));
|
|
38
|
-
};
|
|
9
|
+
export type { TransactionItemTranslations, TransactionItemProps } from "./TransactionItem.types";
|
|
39
10
|
|
|
40
11
|
export const TransactionItem: React.FC<TransactionItemProps> = ({
|
|
41
12
|
transaction,
|
|
@@ -44,89 +15,34 @@ export const TransactionItem: React.FC<TransactionItemProps> = ({
|
|
|
44
15
|
}) => {
|
|
45
16
|
const tokens = useAppDesignTokens();
|
|
46
17
|
|
|
47
|
-
const reasonLabel = useMemo(() =>
|
|
48
|
-
return translations[transaction.reason] || transaction.reason;
|
|
49
|
-
}, [transaction.reason, translations]);
|
|
18
|
+
const reasonLabel = useMemo(() => getReasonLabel(transaction.reason, translations), [transaction.reason, translations]);
|
|
50
19
|
|
|
51
20
|
const isPositive = transaction.change > 0;
|
|
52
21
|
const changeColor = isPositive ? tokens.colors.success : tokens.colors.error;
|
|
53
|
-
const changePrefix =
|
|
22
|
+
const changePrefix = getChangePrefix(transaction.change);
|
|
54
23
|
const iconName = getTransactionIcon(transaction.reason);
|
|
55
24
|
|
|
56
25
|
return (
|
|
57
|
-
<View
|
|
58
|
-
style={[
|
|
59
|
-
styles.container,
|
|
60
|
-
{ backgroundColor: tokens.colors.surfaceSecondary },
|
|
61
|
-
]}
|
|
62
|
-
>
|
|
63
|
-
<View
|
|
64
|
-
style={[
|
|
65
|
-
styles.iconContainer,
|
|
66
|
-
{ backgroundColor: tokens.colors.surface },
|
|
67
|
-
]}
|
|
68
|
-
>
|
|
26
|
+
<View style={[transactionItemStyles.container, { backgroundColor: tokens.colors.surfaceSecondary }]}>
|
|
27
|
+
<View style={[transactionItemStyles.iconContainer, { backgroundColor: tokens.colors.surface }]}>
|
|
69
28
|
<AtomicIcon name={iconName} size="md" color="secondary" />
|
|
70
29
|
</View>
|
|
71
|
-
<View style={
|
|
72
|
-
<AtomicText
|
|
73
|
-
type="bodyMedium"
|
|
74
|
-
style={[styles.reason, { color: tokens.colors.textPrimary }]}
|
|
75
|
-
>
|
|
30
|
+
<View style={transactionItemStyles.content}>
|
|
31
|
+
<AtomicText type="bodyMedium" style={[transactionItemStyles.reason, { color: tokens.colors.textPrimary }]}>
|
|
76
32
|
{reasonLabel}
|
|
77
33
|
</AtomicText>
|
|
78
34
|
{transaction.description && (
|
|
79
|
-
<AtomicText
|
|
80
|
-
type="bodySmall"
|
|
81
|
-
style={{ color: tokens.colors.textSecondary }}
|
|
82
|
-
numberOfLines={1}
|
|
83
|
-
>
|
|
35
|
+
<AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }} numberOfLines={1}>
|
|
84
36
|
{transaction.description}
|
|
85
37
|
</AtomicText>
|
|
86
38
|
)}
|
|
87
|
-
<AtomicText
|
|
88
|
-
type="bodySmall"
|
|
89
|
-
style={{ color: tokens.colors.textSecondary }}
|
|
90
|
-
>
|
|
39
|
+
<AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
|
|
91
40
|
{dateFormatter(transaction.createdAt)}
|
|
92
41
|
</AtomicText>
|
|
93
42
|
</View>
|
|
94
|
-
<AtomicText
|
|
95
|
-
|
|
96
|
-
style={[styles.change, { color: changeColor }]}
|
|
97
|
-
>
|
|
98
|
-
{changePrefix}
|
|
99
|
-
{transaction.change}
|
|
43
|
+
<AtomicText type="titleMedium" style={[transactionItemStyles.change, { color: changeColor }]}>
|
|
44
|
+
{changePrefix}{transaction.change}
|
|
100
45
|
</AtomicText>
|
|
101
46
|
</View>
|
|
102
47
|
);
|
|
103
48
|
};
|
|
104
|
-
|
|
105
|
-
const styles = StyleSheet.create({
|
|
106
|
-
container: {
|
|
107
|
-
flexDirection: "row",
|
|
108
|
-
alignItems: "center",
|
|
109
|
-
padding: 12,
|
|
110
|
-
borderRadius: 12,
|
|
111
|
-
marginBottom: 8,
|
|
112
|
-
},
|
|
113
|
-
iconContainer: {
|
|
114
|
-
width: 40,
|
|
115
|
-
height: 40,
|
|
116
|
-
borderRadius: 20,
|
|
117
|
-
justifyContent: "center",
|
|
118
|
-
alignItems: "center",
|
|
119
|
-
marginRight: 12,
|
|
120
|
-
},
|
|
121
|
-
content: {
|
|
122
|
-
flex: 1,
|
|
123
|
-
gap: 2,
|
|
124
|
-
},
|
|
125
|
-
reason: {
|
|
126
|
-
fontWeight: "600",
|
|
127
|
-
},
|
|
128
|
-
change: {
|
|
129
|
-
fontWeight: "700",
|
|
130
|
-
marginLeft: 12,
|
|
131
|
-
},
|
|
132
|
-
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CreditLog } from "../../domain/types/transaction.types";
|
|
2
|
+
|
|
3
|
+
export interface TransactionItemTranslations {
|
|
4
|
+
purchase: string;
|
|
5
|
+
usage: string;
|
|
6
|
+
refund: string;
|
|
7
|
+
bonus: string;
|
|
8
|
+
subscription: string;
|
|
9
|
+
admin: string;
|
|
10
|
+
reward: string;
|
|
11
|
+
expired: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TransactionItemProps {
|
|
15
|
+
transaction: CreditLog;
|
|
16
|
+
translations: TransactionItemTranslations;
|
|
17
|
+
dateFormatter?: (timestamp: number) => string;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_TRANSACTION_LIST_MAX_HEIGHT = 400;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
|
|
3
|
+
export const transactionListStyles = StyleSheet.create({
|
|
4
|
+
container: {
|
|
5
|
+
marginTop: 24,
|
|
6
|
+
marginBottom: 24,
|
|
7
|
+
},
|
|
8
|
+
header: {
|
|
9
|
+
flexDirection: "row",
|
|
10
|
+
justifyContent: "space-between",
|
|
11
|
+
alignItems: "center",
|
|
12
|
+
marginHorizontal: 16,
|
|
13
|
+
marginBottom: 16,
|
|
14
|
+
},
|
|
15
|
+
title: {
|
|
16
|
+
fontSize: 20,
|
|
17
|
+
fontWeight: "700",
|
|
18
|
+
},
|
|
19
|
+
scrollView: {
|
|
20
|
+
paddingHorizontal: 16,
|
|
21
|
+
},
|
|
22
|
+
scrollContent: {
|
|
23
|
+
paddingBottom: 8,
|
|
24
|
+
},
|
|
25
|
+
stateContainer: {
|
|
26
|
+
padding: 40,
|
|
27
|
+
alignItems: "center",
|
|
28
|
+
gap: 12,
|
|
29
|
+
},
|
|
30
|
+
stateText: {
|
|
31
|
+
fontSize: 14,
|
|
32
|
+
fontWeight: "500",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
@@ -1,23 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transaction List Component
|
|
3
|
-
*
|
|
4
|
-
* Displays a list of credit transactions.
|
|
5
|
-
* Props-driven for full customization.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import React from "react";
|
|
9
|
-
import { View,
|
|
10
|
-
import {
|
|
11
|
-
useAppDesignTokens,
|
|
12
|
-
AtomicText,
|
|
13
|
-
AtomicIcon,
|
|
14
|
-
AtomicSpinner,
|
|
15
|
-
} from "@umituz/react-native-design-system";
|
|
2
|
+
import { View, ScrollView } from "react-native";
|
|
3
|
+
import { useAppDesignTokens, AtomicText, AtomicIcon } from "@umituz/react-native-design-system";
|
|
16
4
|
import type { CreditLog } from "../../domain/types/transaction.types";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from "./
|
|
5
|
+
import { TransactionItem, type TransactionItemTranslations } from "./TransactionItem";
|
|
6
|
+
import { transactionListStyles } from "./TransactionList.styles";
|
|
7
|
+
import { LoadingState, EmptyState } from "./TransactionListStates";
|
|
8
|
+
import { DEFAULT_TRANSACTION_LIST_MAX_HEIGHT } from "./TransactionList.constants";
|
|
21
9
|
|
|
22
10
|
export interface TransactionListTranslations extends TransactionItemTranslations {
|
|
23
11
|
title: string;
|
|
@@ -37,89 +25,35 @@ export const TransactionList: React.FC<TransactionListProps> = ({
|
|
|
37
25
|
transactions,
|
|
38
26
|
loading,
|
|
39
27
|
translations,
|
|
40
|
-
maxHeight =
|
|
28
|
+
maxHeight = DEFAULT_TRANSACTION_LIST_MAX_HEIGHT,
|
|
41
29
|
dateFormatter,
|
|
42
30
|
}) => {
|
|
43
31
|
const tokens = useAppDesignTokens();
|
|
44
32
|
|
|
45
33
|
return (
|
|
46
|
-
<View style={
|
|
47
|
-
<View style={
|
|
48
|
-
<AtomicText
|
|
49
|
-
type="titleLarge"
|
|
50
|
-
style={[styles.title, { color: tokens.colors.textPrimary }]}
|
|
51
|
-
>
|
|
34
|
+
<View style={transactionListStyles.container}>
|
|
35
|
+
<View style={transactionListStyles.header}>
|
|
36
|
+
<AtomicText type="titleLarge" style={[transactionListStyles.title, { color: tokens.colors.textPrimary }]}>
|
|
52
37
|
{translations.title}
|
|
53
38
|
</AtomicText>
|
|
54
39
|
<AtomicIcon name="time-outline" size="md" color="secondary" />
|
|
55
40
|
</View>
|
|
56
41
|
|
|
57
42
|
{loading ? (
|
|
58
|
-
<
|
|
59
|
-
size="lg"
|
|
60
|
-
color="primary"
|
|
61
|
-
text={translations.loading}
|
|
62
|
-
style={styles.stateContainer}
|
|
63
|
-
/>
|
|
43
|
+
<LoadingState message={translations.loading} />
|
|
64
44
|
) : transactions.length === 0 ? (
|
|
65
|
-
<
|
|
66
|
-
<AtomicIcon name="file-tray-outline" size="xl" color="secondary" />
|
|
67
|
-
<AtomicText
|
|
68
|
-
type="bodyMedium"
|
|
69
|
-
style={[styles.stateText, { color: tokens.colors.textSecondary }]}
|
|
70
|
-
>
|
|
71
|
-
{translations.empty}
|
|
72
|
-
</AtomicText>
|
|
73
|
-
</View>
|
|
45
|
+
<EmptyState message={translations.empty} />
|
|
74
46
|
) : (
|
|
75
47
|
<ScrollView
|
|
76
|
-
style={[
|
|
77
|
-
contentContainerStyle={
|
|
48
|
+
style={[transactionListStyles.scrollView, { maxHeight }]}
|
|
49
|
+
contentContainerStyle={transactionListStyles.scrollContent}
|
|
78
50
|
showsVerticalScrollIndicator={false}
|
|
79
51
|
>
|
|
80
52
|
{transactions.map((transaction) => (
|
|
81
|
-
<TransactionItem
|
|
82
|
-
key={transaction.id}
|
|
83
|
-
transaction={transaction}
|
|
84
|
-
translations={translations}
|
|
85
|
-
dateFormatter={dateFormatter}
|
|
86
|
-
/>
|
|
53
|
+
<TransactionItem key={transaction.id} transaction={transaction} translations={translations} dateFormatter={dateFormatter} />
|
|
87
54
|
))}
|
|
88
55
|
</ScrollView>
|
|
89
56
|
)}
|
|
90
57
|
</View>
|
|
91
58
|
);
|
|
92
59
|
};
|
|
93
|
-
|
|
94
|
-
const styles = StyleSheet.create({
|
|
95
|
-
container: {
|
|
96
|
-
marginTop: 24,
|
|
97
|
-
marginBottom: 24,
|
|
98
|
-
},
|
|
99
|
-
header: {
|
|
100
|
-
flexDirection: "row",
|
|
101
|
-
justifyContent: "space-between",
|
|
102
|
-
alignItems: "center",
|
|
103
|
-
marginHorizontal: 16,
|
|
104
|
-
marginBottom: 16,
|
|
105
|
-
},
|
|
106
|
-
title: {
|
|
107
|
-
fontSize: 20,
|
|
108
|
-
fontWeight: "700",
|
|
109
|
-
},
|
|
110
|
-
scrollView: {
|
|
111
|
-
paddingHorizontal: 16,
|
|
112
|
-
},
|
|
113
|
-
scrollContent: {
|
|
114
|
-
paddingBottom: 8,
|
|
115
|
-
},
|
|
116
|
-
stateContainer: {
|
|
117
|
-
padding: 40,
|
|
118
|
-
alignItems: "center",
|
|
119
|
-
gap: 12,
|
|
120
|
-
},
|
|
121
|
-
stateText: {
|
|
122
|
-
fontSize: 14,
|
|
123
|
-
fontWeight: "500",
|
|
124
|
-
},
|
|
125
|
-
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { useAppDesignTokens, AtomicText, AtomicIcon, AtomicSpinner } from "@umituz/react-native-design-system";
|
|
4
|
+
import { transactionListStyles } from "./TransactionList.styles";
|
|
5
|
+
|
|
6
|
+
interface LoadingStateProps {
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const LoadingState: React.FC<LoadingStateProps> = ({ message }) => (
|
|
11
|
+
<AtomicSpinner size="lg" color="primary" text={message} style={transactionListStyles.stateContainer} />
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
interface EmptyStateProps {
|
|
15
|
+
message: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const EmptyState: React.FC<EmptyStateProps> = ({ message }) => {
|
|
19
|
+
const tokens = useAppDesignTokens();
|
|
20
|
+
return (
|
|
21
|
+
<View style={transactionListStyles.stateContainer}>
|
|
22
|
+
<AtomicIcon name="file-tray-outline" size="xl" color="secondary" />
|
|
23
|
+
<AtomicText type="bodyMedium" style={[transactionListStyles.stateText, { color: tokens.colors.textSecondary }]}>
|
|
24
|
+
{message}
|
|
25
|
+
</AtomicText>
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { timezoneService } from "@umituz/react-native-design-system";
|
|
2
|
+
import type { TransactionItemTranslations } from "./TransactionItem.types";
|
|
3
|
+
|
|
4
|
+
export const defaultDateFormatter = (timestamp: number): string => {
|
|
5
|
+
return timezoneService.formatToDisplayDateTime(new Date(timestamp));
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const getReasonLabel = (reason: string, translations: TransactionItemTranslations): string => {
|
|
9
|
+
return translations[reason as keyof TransactionItemTranslations] || reason;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const getChangePrefix = (change: number): string => {
|
|
13
|
+
return change > 0 ? "+" : "";
|
|
14
|
+
};
|