@umituz/react-native-subscription 2.14.10 → 2.14.12

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.
@@ -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";