@umituz/react-native-subscription 2.14.9 → 2.14.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/paywall/components/PaywallTabBar.tsx +1 -1
- package/src/domains/wallet/domain/entities/CreditCost.ts +45 -0
- package/src/domains/wallet/domain/errors/WalletError.ts +169 -0
- package/src/domains/wallet/domain/types/credit-cost.types.ts +86 -0
- package/src/domains/wallet/domain/types/index.ts +33 -0
- package/src/domains/wallet/domain/types/transaction.types.ts +49 -0
- package/src/domains/wallet/domain/types/wallet.types.ts +50 -0
- package/src/domains/wallet/index.ts +116 -0
- package/src/domains/wallet/infrastructure/repositories/TransactionRepository.ts +166 -0
- package/src/domains/wallet/infrastructure/services/ProductMetadataService.ts +131 -0
- package/src/domains/wallet/presentation/components/BalanceCard.tsx +119 -0
- package/src/domains/wallet/presentation/components/TransactionItem.tsx +150 -0
- package/src/domains/wallet/presentation/components/TransactionList.tsx +127 -0
- package/src/domains/wallet/presentation/components/index.ts +23 -0
- package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +88 -0
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +87 -0
- package/src/domains/wallet/presentation/hooks/useWallet.ts +93 -0
- package/src/domains/wallet/presentation/screens/WalletScreen.tsx +147 -0
- package/src/index.ts +12 -1
- package/src/presentation/components/feedback/PaywallFeedbackModal.tsx +12 -14
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction Repository
|
|
3
|
+
*
|
|
4
|
+
* Firestore operations for credit transaction logs.
|
|
5
|
+
* Generic repository for use across hundreds of apps.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
collection,
|
|
10
|
+
getDocs,
|
|
11
|
+
addDoc,
|
|
12
|
+
query,
|
|
13
|
+
where,
|
|
14
|
+
orderBy,
|
|
15
|
+
limit as firestoreLimit,
|
|
16
|
+
serverTimestamp,
|
|
17
|
+
type Firestore,
|
|
18
|
+
type QueryConstraint,
|
|
19
|
+
} from "firebase/firestore";
|
|
20
|
+
import { BaseRepository, getFirestore } from "@umituz/react-native-firebase";
|
|
21
|
+
import type {
|
|
22
|
+
CreditLog,
|
|
23
|
+
TransactionRepositoryConfig,
|
|
24
|
+
TransactionQueryOptions,
|
|
25
|
+
TransactionResult,
|
|
26
|
+
TransactionReason,
|
|
27
|
+
} from "../../domain/types/transaction.types";
|
|
28
|
+
|
|
29
|
+
export class TransactionRepository extends BaseRepository {
|
|
30
|
+
private config: TransactionRepositoryConfig;
|
|
31
|
+
|
|
32
|
+
constructor(config: TransactionRepositoryConfig) {
|
|
33
|
+
super();
|
|
34
|
+
this.config = config;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private getCollectionRef(db: Firestore, userId: string) {
|
|
38
|
+
if (this.config.useUserSubcollection) {
|
|
39
|
+
return collection(db, "users", userId, this.config.collectionName);
|
|
40
|
+
}
|
|
41
|
+
return collection(db, this.config.collectionName);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async getTransactions(
|
|
45
|
+
options: TransactionQueryOptions
|
|
46
|
+
): Promise<TransactionResult> {
|
|
47
|
+
const db = getFirestore();
|
|
48
|
+
if (!db) {
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
error: { message: "Database not available", code: "DB_NOT_AVAILABLE" },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const colRef = this.getCollectionRef(db, options.userId);
|
|
57
|
+
const constraints: QueryConstraint[] = [];
|
|
58
|
+
|
|
59
|
+
if (!this.config.useUserSubcollection) {
|
|
60
|
+
constraints.push(where("userId", "==", options.userId));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
constraints.push(orderBy("createdAt", "desc"));
|
|
64
|
+
constraints.push(firestoreLimit(options.limit ?? 50));
|
|
65
|
+
|
|
66
|
+
const q = query(colRef, ...constraints);
|
|
67
|
+
const snapshot = await getDocs(q);
|
|
68
|
+
|
|
69
|
+
const transactions: CreditLog[] = snapshot.docs.map((docSnap) => {
|
|
70
|
+
const data = docSnap.data();
|
|
71
|
+
return {
|
|
72
|
+
id: docSnap.id,
|
|
73
|
+
userId: data.userId || options.userId,
|
|
74
|
+
change: data.change,
|
|
75
|
+
reason: data.reason,
|
|
76
|
+
feature: data.feature,
|
|
77
|
+
jobId: data.jobId,
|
|
78
|
+
packageId: data.packageId,
|
|
79
|
+
subscriptionPlan: data.subscriptionPlan,
|
|
80
|
+
description: data.description,
|
|
81
|
+
createdAt: data.createdAt?.toMillis?.() || Date.now(),
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return { success: true, data: transactions };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (__DEV__) {
|
|
88
|
+
console.error("[TransactionRepository] Error:", error);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
error: {
|
|
93
|
+
message:
|
|
94
|
+
error instanceof Error ? error.message : "Failed to get logs",
|
|
95
|
+
code: "FETCH_FAILED",
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async addTransaction(
|
|
102
|
+
userId: string,
|
|
103
|
+
change: number,
|
|
104
|
+
reason: TransactionReason,
|
|
105
|
+
metadata?: Partial<CreditLog>
|
|
106
|
+
): Promise<TransactionResult<CreditLog>> {
|
|
107
|
+
const db = getFirestore();
|
|
108
|
+
if (!db) {
|
|
109
|
+
return {
|
|
110
|
+
success: false,
|
|
111
|
+
error: { message: "Database not available", code: "DB_NOT_AVAILABLE" },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const colRef = this.getCollectionRef(db, userId);
|
|
117
|
+
const docData = {
|
|
118
|
+
userId,
|
|
119
|
+
change,
|
|
120
|
+
reason,
|
|
121
|
+
feature: metadata?.feature,
|
|
122
|
+
jobId: metadata?.jobId,
|
|
123
|
+
packageId: metadata?.packageId,
|
|
124
|
+
subscriptionPlan: metadata?.subscriptionPlan,
|
|
125
|
+
description: metadata?.description,
|
|
126
|
+
createdAt: serverTimestamp(),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const docRef = await addDoc(colRef, docData);
|
|
130
|
+
|
|
131
|
+
if (__DEV__) {
|
|
132
|
+
console.log("[TransactionRepository] Added:", docRef.id);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
success: true,
|
|
137
|
+
data: {
|
|
138
|
+
id: docRef.id,
|
|
139
|
+
userId,
|
|
140
|
+
change,
|
|
141
|
+
reason,
|
|
142
|
+
...metadata,
|
|
143
|
+
createdAt: Date.now(),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (__DEV__) {
|
|
148
|
+
console.error("[TransactionRepository] Add error:", error);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
success: false,
|
|
152
|
+
error: {
|
|
153
|
+
message:
|
|
154
|
+
error instanceof Error ? error.message : "Failed to add log",
|
|
155
|
+
code: "ADD_FAILED",
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function createTransactionRepository(
|
|
163
|
+
config: TransactionRepositoryConfig
|
|
164
|
+
): TransactionRepository {
|
|
165
|
+
return new TransactionRepository(config);
|
|
166
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product Metadata Service
|
|
3
|
+
*
|
|
4
|
+
* Generic service for fetching product metadata from Firestore.
|
|
5
|
+
* Collection name is configurable for use across hundreds of apps.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { collection, getDocs, orderBy, query } from "firebase/firestore";
|
|
9
|
+
import { getFirestore } from "@umituz/react-native-firebase";
|
|
10
|
+
import type {
|
|
11
|
+
ProductMetadata,
|
|
12
|
+
ProductMetadataConfig,
|
|
13
|
+
ProductType,
|
|
14
|
+
} from "../../domain/types/wallet.types";
|
|
15
|
+
|
|
16
|
+
interface CacheEntry {
|
|
17
|
+
data: ProductMetadata[];
|
|
18
|
+
timestamp: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
22
|
+
|
|
23
|
+
export class ProductMetadataService {
|
|
24
|
+
private config: ProductMetadataConfig;
|
|
25
|
+
private cache: CacheEntry | null = null;
|
|
26
|
+
|
|
27
|
+
constructor(config: ProductMetadataConfig) {
|
|
28
|
+
this.config = config;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private isCacheValid(): boolean {
|
|
32
|
+
if (!this.cache) return false;
|
|
33
|
+
const ttl = this.config.cacheTTL ?? DEFAULT_CACHE_TTL_MS;
|
|
34
|
+
return Date.now() - this.cache.timestamp < ttl;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private async fetchFromFirebase(): Promise<ProductMetadata[]> {
|
|
38
|
+
const db = getFirestore();
|
|
39
|
+
if (!db) {
|
|
40
|
+
throw new Error("Firestore not initialized");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const colRef = collection(db, this.config.collectionName);
|
|
44
|
+
const q = query(colRef, orderBy("order", "asc"));
|
|
45
|
+
const snapshot = await getDocs(q);
|
|
46
|
+
|
|
47
|
+
return snapshot.docs.map((docSnap) => ({
|
|
48
|
+
productId: docSnap.id,
|
|
49
|
+
...docSnap.data(),
|
|
50
|
+
})) as ProductMetadata[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async getAll(): Promise<ProductMetadata[]> {
|
|
54
|
+
if (this.isCacheValid() && this.cache) {
|
|
55
|
+
return this.cache.data;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const data = await this.fetchFromFirebase();
|
|
60
|
+
this.cache = { data, timestamp: Date.now() };
|
|
61
|
+
|
|
62
|
+
if (__DEV__) {
|
|
63
|
+
console.log(
|
|
64
|
+
"[ProductMetadataService] Loaded:",
|
|
65
|
+
data.length,
|
|
66
|
+
"products"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return data;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (__DEV__) {
|
|
73
|
+
console.error("[ProductMetadataService] Fetch error:", error);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this.cache) {
|
|
77
|
+
return this.cache.data;
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async getByProductId(productId: string): Promise<ProductMetadata | null> {
|
|
84
|
+
const all = await this.getAll();
|
|
85
|
+
return all.find((p) => p.productId === productId) ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async getByType(type: ProductType): Promise<ProductMetadata[]> {
|
|
89
|
+
const all = await this.getAll();
|
|
90
|
+
return all.filter((p) => p.type === type);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async getCreditsPackages(): Promise<ProductMetadata[]> {
|
|
94
|
+
return this.getByType("credits");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async getSubscriptionPackages(): Promise<ProductMetadata[]> {
|
|
98
|
+
return this.getByType("subscription");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
clearCache(): void {
|
|
102
|
+
this.cache = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createProductMetadataService(
|
|
107
|
+
config: ProductMetadataConfig
|
|
108
|
+
): ProductMetadataService {
|
|
109
|
+
return new ProductMetadataService(config);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let defaultService: ProductMetadataService | null = null;
|
|
113
|
+
|
|
114
|
+
export function configureProductMetadataService(
|
|
115
|
+
config: ProductMetadataConfig
|
|
116
|
+
): void {
|
|
117
|
+
defaultService = new ProductMetadataService(config);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getProductMetadataService(): ProductMetadataService {
|
|
121
|
+
if (!defaultService) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
"ProductMetadataService not configured. Call configureProductMetadataService first."
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return defaultService;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function resetProductMetadataService(): void {
|
|
130
|
+
defaultService = null;
|
|
131
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Balance Card Component
|
|
3
|
+
*
|
|
4
|
+
* Displays user's credit balance with gradient background.
|
|
5
|
+
* Props-driven for full customization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { View, StyleSheet } from "react-native";
|
|
10
|
+
import { LinearGradient } from "expo-linear-gradient";
|
|
11
|
+
import {
|
|
12
|
+
useAppDesignTokens,
|
|
13
|
+
AtomicText,
|
|
14
|
+
AtomicIcon,
|
|
15
|
+
} from "@umituz/react-native-design-system";
|
|
16
|
+
|
|
17
|
+
export interface BalanceCardTranslations {
|
|
18
|
+
balanceLabel: string;
|
|
19
|
+
availableCredits: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface BalanceCardProps {
|
|
23
|
+
balance: number;
|
|
24
|
+
translations: BalanceCardTranslations;
|
|
25
|
+
iconName?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const BalanceCard: React.FC<BalanceCardProps> = ({
|
|
29
|
+
balance,
|
|
30
|
+
translations,
|
|
31
|
+
iconName = "Wallet",
|
|
32
|
+
}) => {
|
|
33
|
+
const tokens = useAppDesignTokens();
|
|
34
|
+
const gradientColors = [
|
|
35
|
+
tokens.colors.primary,
|
|
36
|
+
tokens.colors.primaryDark || tokens.colors.primary,
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<LinearGradient
|
|
41
|
+
colors={gradientColors}
|
|
42
|
+
start={{ x: 0, y: 0 }}
|
|
43
|
+
end={{ x: 1, y: 1 }}
|
|
44
|
+
style={styles.container}
|
|
45
|
+
>
|
|
46
|
+
<View style={styles.content}>
|
|
47
|
+
<View style={styles.textContainer}>
|
|
48
|
+
<AtomicText
|
|
49
|
+
type="bodyMedium"
|
|
50
|
+
style={[styles.label, { color: tokens.colors.onPrimary + "CC" }]}
|
|
51
|
+
>
|
|
52
|
+
{translations.balanceLabel}
|
|
53
|
+
</AtomicText>
|
|
54
|
+
<AtomicText
|
|
55
|
+
type="displayLarge"
|
|
56
|
+
style={[styles.balance, { color: tokens.colors.onPrimary }]}
|
|
57
|
+
>
|
|
58
|
+
{balance.toLocaleString()}
|
|
59
|
+
</AtomicText>
|
|
60
|
+
<AtomicText
|
|
61
|
+
type="bodySmall"
|
|
62
|
+
style={[styles.subtitle, { color: tokens.colors.onPrimary + "CC" }]}
|
|
63
|
+
>
|
|
64
|
+
{translations.availableCredits}
|
|
65
|
+
</AtomicText>
|
|
66
|
+
</View>
|
|
67
|
+
<View
|
|
68
|
+
style={[
|
|
69
|
+
styles.iconContainer,
|
|
70
|
+
{ backgroundColor: tokens.colors.onPrimary + "20" },
|
|
71
|
+
]}
|
|
72
|
+
>
|
|
73
|
+
<AtomicIcon name={iconName} size="xl" color="onPrimary" />
|
|
74
|
+
</View>
|
|
75
|
+
</View>
|
|
76
|
+
</LinearGradient>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const styles = StyleSheet.create({
|
|
81
|
+
container: {
|
|
82
|
+
borderRadius: 16,
|
|
83
|
+
padding: 24,
|
|
84
|
+
marginHorizontal: 16,
|
|
85
|
+
marginTop: 16,
|
|
86
|
+
marginBottom: 8,
|
|
87
|
+
},
|
|
88
|
+
content: {
|
|
89
|
+
flexDirection: "row",
|
|
90
|
+
justifyContent: "space-between",
|
|
91
|
+
alignItems: "center",
|
|
92
|
+
},
|
|
93
|
+
textContainer: {
|
|
94
|
+
flex: 1,
|
|
95
|
+
},
|
|
96
|
+
label: {
|
|
97
|
+
fontSize: 14,
|
|
98
|
+
fontWeight: "500",
|
|
99
|
+
marginBottom: 8,
|
|
100
|
+
opacity: 0.9,
|
|
101
|
+
},
|
|
102
|
+
balance: {
|
|
103
|
+
fontSize: 36,
|
|
104
|
+
fontWeight: "700",
|
|
105
|
+
marginBottom: 4,
|
|
106
|
+
},
|
|
107
|
+
subtitle: {
|
|
108
|
+
fontSize: 12,
|
|
109
|
+
fontWeight: "400",
|
|
110
|
+
opacity: 0.8,
|
|
111
|
+
},
|
|
112
|
+
iconContainer: {
|
|
113
|
+
width: 64,
|
|
114
|
+
height: 64,
|
|
115
|
+
borderRadius: 32,
|
|
116
|
+
justifyContent: "center",
|
|
117
|
+
alignItems: "center",
|
|
118
|
+
},
|
|
119
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction Item Component
|
|
3
|
+
*
|
|
4
|
+
* Displays a single credit transaction.
|
|
5
|
+
* Props-driven for full customization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { useMemo } from "react";
|
|
9
|
+
import { View, StyleSheet } from "react-native";
|
|
10
|
+
import {
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
AtomicText,
|
|
13
|
+
AtomicIcon,
|
|
14
|
+
} from "@umituz/react-native-design-system";
|
|
15
|
+
import type { CreditLog, TransactionReason } from "../../domain/types/transaction.types";
|
|
16
|
+
|
|
17
|
+
export interface TransactionItemTranslations {
|
|
18
|
+
purchase: string;
|
|
19
|
+
usage: string;
|
|
20
|
+
refund: string;
|
|
21
|
+
bonus: string;
|
|
22
|
+
subscription: string;
|
|
23
|
+
admin: string;
|
|
24
|
+
reward: string;
|
|
25
|
+
expired: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TransactionItemProps {
|
|
29
|
+
transaction: CreditLog;
|
|
30
|
+
translations: TransactionItemTranslations;
|
|
31
|
+
dateFormatter?: (timestamp: number) => string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const getReasonIcon = (reason: TransactionReason): string => {
|
|
35
|
+
const iconMap: Record<TransactionReason, string> = {
|
|
36
|
+
purchase: "ShoppingCart",
|
|
37
|
+
usage: "Zap",
|
|
38
|
+
refund: "RotateCcw",
|
|
39
|
+
bonus: "Gift",
|
|
40
|
+
subscription: "Star",
|
|
41
|
+
admin: "Shield",
|
|
42
|
+
reward: "Award",
|
|
43
|
+
expired: "Clock",
|
|
44
|
+
};
|
|
45
|
+
return iconMap[reason] || "Circle";
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const defaultDateFormatter = (timestamp: number): string => {
|
|
49
|
+
const date = new Date(timestamp);
|
|
50
|
+
return date.toLocaleDateString(undefined, {
|
|
51
|
+
month: "short",
|
|
52
|
+
day: "numeric",
|
|
53
|
+
hour: "2-digit",
|
|
54
|
+
minute: "2-digit",
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const TransactionItem: React.FC<TransactionItemProps> = ({
|
|
59
|
+
transaction,
|
|
60
|
+
translations,
|
|
61
|
+
dateFormatter = defaultDateFormatter,
|
|
62
|
+
}) => {
|
|
63
|
+
const tokens = useAppDesignTokens();
|
|
64
|
+
|
|
65
|
+
const reasonLabel = useMemo(() => {
|
|
66
|
+
return translations[transaction.reason] || transaction.reason;
|
|
67
|
+
}, [transaction.reason, translations]);
|
|
68
|
+
|
|
69
|
+
const isPositive = transaction.change > 0;
|
|
70
|
+
const changeColor = isPositive ? tokens.colors.success : tokens.colors.error;
|
|
71
|
+
const changePrefix = isPositive ? "+" : "";
|
|
72
|
+
const iconName = getReasonIcon(transaction.reason);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<View
|
|
76
|
+
style={[
|
|
77
|
+
styles.container,
|
|
78
|
+
{ backgroundColor: tokens.colors.surfaceSecondary },
|
|
79
|
+
]}
|
|
80
|
+
>
|
|
81
|
+
<View
|
|
82
|
+
style={[
|
|
83
|
+
styles.iconContainer,
|
|
84
|
+
{ backgroundColor: tokens.colors.surface },
|
|
85
|
+
]}
|
|
86
|
+
>
|
|
87
|
+
<AtomicIcon name={iconName} size="md" color="secondary" />
|
|
88
|
+
</View>
|
|
89
|
+
<View style={styles.content}>
|
|
90
|
+
<AtomicText
|
|
91
|
+
type="bodyMedium"
|
|
92
|
+
style={[styles.reason, { color: tokens.colors.textPrimary }]}
|
|
93
|
+
>
|
|
94
|
+
{reasonLabel}
|
|
95
|
+
</AtomicText>
|
|
96
|
+
{transaction.description && (
|
|
97
|
+
<AtomicText
|
|
98
|
+
type="bodySmall"
|
|
99
|
+
style={{ color: tokens.colors.textSecondary }}
|
|
100
|
+
numberOfLines={1}
|
|
101
|
+
>
|
|
102
|
+
{transaction.description}
|
|
103
|
+
</AtomicText>
|
|
104
|
+
)}
|
|
105
|
+
<AtomicText
|
|
106
|
+
type="bodySmall"
|
|
107
|
+
style={{ color: tokens.colors.textSecondary }}
|
|
108
|
+
>
|
|
109
|
+
{dateFormatter(transaction.createdAt)}
|
|
110
|
+
</AtomicText>
|
|
111
|
+
</View>
|
|
112
|
+
<AtomicText
|
|
113
|
+
type="titleMedium"
|
|
114
|
+
style={[styles.change, { color: changeColor }]}
|
|
115
|
+
>
|
|
116
|
+
{changePrefix}
|
|
117
|
+
{transaction.change}
|
|
118
|
+
</AtomicText>
|
|
119
|
+
</View>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const styles = StyleSheet.create({
|
|
124
|
+
container: {
|
|
125
|
+
flexDirection: "row",
|
|
126
|
+
alignItems: "center",
|
|
127
|
+
padding: 12,
|
|
128
|
+
borderRadius: 12,
|
|
129
|
+
marginBottom: 8,
|
|
130
|
+
},
|
|
131
|
+
iconContainer: {
|
|
132
|
+
width: 40,
|
|
133
|
+
height: 40,
|
|
134
|
+
borderRadius: 20,
|
|
135
|
+
justifyContent: "center",
|
|
136
|
+
alignItems: "center",
|
|
137
|
+
marginRight: 12,
|
|
138
|
+
},
|
|
139
|
+
content: {
|
|
140
|
+
flex: 1,
|
|
141
|
+
gap: 2,
|
|
142
|
+
},
|
|
143
|
+
reason: {
|
|
144
|
+
fontWeight: "600",
|
|
145
|
+
},
|
|
146
|
+
change: {
|
|
147
|
+
fontWeight: "700",
|
|
148
|
+
marginLeft: 12,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction List Component
|
|
3
|
+
*
|
|
4
|
+
* Displays a list of credit transactions.
|
|
5
|
+
* Props-driven for full customization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { View, StyleSheet, ScrollView, ActivityIndicator } from "react-native";
|
|
10
|
+
import {
|
|
11
|
+
useAppDesignTokens,
|
|
12
|
+
AtomicText,
|
|
13
|
+
AtomicIcon,
|
|
14
|
+
} from "@umituz/react-native-design-system";
|
|
15
|
+
import type { CreditLog } from "../../domain/types/transaction.types";
|
|
16
|
+
import {
|
|
17
|
+
TransactionItem,
|
|
18
|
+
type TransactionItemTranslations,
|
|
19
|
+
} from "./TransactionItem";
|
|
20
|
+
|
|
21
|
+
export interface TransactionListTranslations extends TransactionItemTranslations {
|
|
22
|
+
title: string;
|
|
23
|
+
empty: string;
|
|
24
|
+
loading: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TransactionListProps {
|
|
28
|
+
transactions: CreditLog[];
|
|
29
|
+
loading: boolean;
|
|
30
|
+
translations: TransactionListTranslations;
|
|
31
|
+
maxHeight?: number;
|
|
32
|
+
dateFormatter?: (timestamp: number) => string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const TransactionList: React.FC<TransactionListProps> = ({
|
|
36
|
+
transactions,
|
|
37
|
+
loading,
|
|
38
|
+
translations,
|
|
39
|
+
maxHeight = 400,
|
|
40
|
+
dateFormatter,
|
|
41
|
+
}) => {
|
|
42
|
+
const tokens = useAppDesignTokens();
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<View style={styles.container}>
|
|
46
|
+
<View style={styles.header}>
|
|
47
|
+
<AtomicText
|
|
48
|
+
type="titleLarge"
|
|
49
|
+
style={[styles.title, { color: tokens.colors.textPrimary }]}
|
|
50
|
+
>
|
|
51
|
+
{translations.title}
|
|
52
|
+
</AtomicText>
|
|
53
|
+
<AtomicIcon name="History" size="md" color="secondary" />
|
|
54
|
+
</View>
|
|
55
|
+
|
|
56
|
+
{loading ? (
|
|
57
|
+
<View style={styles.stateContainer}>
|
|
58
|
+
<ActivityIndicator size="large" color={tokens.colors.primary} />
|
|
59
|
+
<AtomicText
|
|
60
|
+
type="bodyMedium"
|
|
61
|
+
style={[styles.stateText, { color: tokens.colors.textSecondary }]}
|
|
62
|
+
>
|
|
63
|
+
{translations.loading}
|
|
64
|
+
</AtomicText>
|
|
65
|
+
</View>
|
|
66
|
+
) : transactions.length === 0 ? (
|
|
67
|
+
<View style={styles.stateContainer}>
|
|
68
|
+
<AtomicIcon name="Package" size="xl" color="secondary" />
|
|
69
|
+
<AtomicText
|
|
70
|
+
type="bodyMedium"
|
|
71
|
+
style={[styles.stateText, { color: tokens.colors.textSecondary }]}
|
|
72
|
+
>
|
|
73
|
+
{translations.empty}
|
|
74
|
+
</AtomicText>
|
|
75
|
+
</View>
|
|
76
|
+
) : (
|
|
77
|
+
<ScrollView
|
|
78
|
+
style={[styles.scrollView, { maxHeight }]}
|
|
79
|
+
contentContainerStyle={styles.scrollContent}
|
|
80
|
+
showsVerticalScrollIndicator={false}
|
|
81
|
+
>
|
|
82
|
+
{transactions.map((transaction) => (
|
|
83
|
+
<TransactionItem
|
|
84
|
+
key={transaction.id}
|
|
85
|
+
transaction={transaction}
|
|
86
|
+
translations={translations}
|
|
87
|
+
dateFormatter={dateFormatter}
|
|
88
|
+
/>
|
|
89
|
+
))}
|
|
90
|
+
</ScrollView>
|
|
91
|
+
)}
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const styles = StyleSheet.create({
|
|
97
|
+
container: {
|
|
98
|
+
marginTop: 24,
|
|
99
|
+
marginBottom: 24,
|
|
100
|
+
},
|
|
101
|
+
header: {
|
|
102
|
+
flexDirection: "row",
|
|
103
|
+
justifyContent: "space-between",
|
|
104
|
+
alignItems: "center",
|
|
105
|
+
marginHorizontal: 16,
|
|
106
|
+
marginBottom: 16,
|
|
107
|
+
},
|
|
108
|
+
title: {
|
|
109
|
+
fontSize: 20,
|
|
110
|
+
fontWeight: "700",
|
|
111
|
+
},
|
|
112
|
+
scrollView: {
|
|
113
|
+
paddingHorizontal: 16,
|
|
114
|
+
},
|
|
115
|
+
scrollContent: {
|
|
116
|
+
paddingBottom: 8,
|
|
117
|
+
},
|
|
118
|
+
stateContainer: {
|
|
119
|
+
padding: 40,
|
|
120
|
+
alignItems: "center",
|
|
121
|
+
gap: 12,
|
|
122
|
+
},
|
|
123
|
+
stateText: {
|
|
124
|
+
fontSize: 14,
|
|
125
|
+
fontWeight: "500",
|
|
126
|
+
},
|
|
127
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet Components Index
|
|
3
|
+
*
|
|
4
|
+
* Export all wallet-related components.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
BalanceCard,
|
|
9
|
+
type BalanceCardProps,
|
|
10
|
+
type BalanceCardTranslations,
|
|
11
|
+
} from "./BalanceCard";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
TransactionItem,
|
|
15
|
+
type TransactionItemProps,
|
|
16
|
+
type TransactionItemTranslations,
|
|
17
|
+
} from "./TransactionItem";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
TransactionList,
|
|
21
|
+
type TransactionListProps,
|
|
22
|
+
type TransactionListTranslations,
|
|
23
|
+
} from "./TransactionList";
|