@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,88 @@
1
+ /**
2
+ * useProductMetadata Hook
3
+ *
4
+ * TanStack Query hook for fetching product metadata.
5
+ * Generic and reusable - uses config from ProductMetadataService.
6
+ */
7
+
8
+ import { useQuery } from "@tanstack/react-query";
9
+ import type {
10
+ ProductMetadata,
11
+ ProductMetadataConfig,
12
+ ProductType,
13
+ } from "../../domain/types/wallet.types";
14
+ import { ProductMetadataService } from "../../infrastructure/services/ProductMetadataService";
15
+
16
+ const CACHE_CONFIG = {
17
+ staleTime: 5 * 60 * 1000, // 5 minutes
18
+ gcTime: 30 * 60 * 1000, // 30 minutes
19
+ };
20
+
21
+ export const productMetadataQueryKeys = {
22
+ all: ["productMetadata"] as const,
23
+ byType: (type: ProductType) => ["productMetadata", type] as const,
24
+ };
25
+
26
+ export interface UseProductMetadataParams {
27
+ config: ProductMetadataConfig;
28
+ type?: ProductType;
29
+ enabled?: boolean;
30
+ }
31
+
32
+ export interface UseProductMetadataResult {
33
+ products: ProductMetadata[];
34
+ isLoading: boolean;
35
+ error: Error | null;
36
+ refetch: () => void;
37
+ creditsPackages: ProductMetadata[];
38
+ subscriptionPackages: ProductMetadata[];
39
+ }
40
+
41
+ export function useProductMetadata({
42
+ config,
43
+ type,
44
+ enabled = true,
45
+ }: UseProductMetadataParams): UseProductMetadataResult {
46
+ const service = new ProductMetadataService(config);
47
+
48
+ const queryKey = type
49
+ ? productMetadataQueryKeys.byType(type)
50
+ : productMetadataQueryKeys.all;
51
+
52
+ const { data, isLoading, error, refetch } = useQuery({
53
+ queryKey,
54
+ queryFn: async () => {
55
+ if (type) {
56
+ return service.getByType(type);
57
+ }
58
+ return service.getAll();
59
+ },
60
+ enabled,
61
+ staleTime: CACHE_CONFIG.staleTime,
62
+ gcTime: CACHE_CONFIG.gcTime,
63
+ });
64
+
65
+ const products = data ?? [];
66
+
67
+ const creditsPackages = products.filter((p) => p.type === "credits");
68
+ const subscriptionPackages = products.filter((p) => p.type === "subscription");
69
+
70
+ if (__DEV__) {
71
+ console.log("[useProductMetadata] State", {
72
+ enabled,
73
+ isLoading,
74
+ count: products.length,
75
+ credits: creditsPackages.length,
76
+ subscriptions: subscriptionPackages.length,
77
+ });
78
+ }
79
+
80
+ return {
81
+ products,
82
+ isLoading,
83
+ error: error as Error | null,
84
+ refetch,
85
+ creditsPackages,
86
+ subscriptionPackages,
87
+ };
88
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * useTransactionHistory Hook
3
+ *
4
+ * TanStack Query hook for fetching credit transaction history.
5
+ * Generic and reusable - uses config from TransactionRepository.
6
+ */
7
+
8
+ import { useQuery } from "@tanstack/react-query";
9
+ import type {
10
+ CreditLog,
11
+ TransactionRepositoryConfig,
12
+ } from "../../domain/types/transaction.types";
13
+ import { TransactionRepository } from "../../infrastructure/repositories/TransactionRepository";
14
+
15
+ const CACHE_CONFIG = {
16
+ staleTime: 60 * 1000, // 1 minute
17
+ gcTime: 5 * 60 * 1000, // 5 minutes
18
+ };
19
+
20
+ export const transactionQueryKeys = {
21
+ all: ["transactions"] as const,
22
+ user: (userId: string) => ["transactions", userId] as const,
23
+ };
24
+
25
+ export interface UseTransactionHistoryParams {
26
+ userId: string | undefined;
27
+ config: TransactionRepositoryConfig;
28
+ limit?: number;
29
+ enabled?: boolean;
30
+ }
31
+
32
+ export interface UseTransactionHistoryResult {
33
+ transactions: CreditLog[];
34
+ isLoading: boolean;
35
+ error: Error | null;
36
+ refetch: () => void;
37
+ isEmpty: boolean;
38
+ }
39
+
40
+ export function useTransactionHistory({
41
+ userId,
42
+ config,
43
+ limit = 50,
44
+ enabled = true,
45
+ }: UseTransactionHistoryParams): UseTransactionHistoryResult {
46
+ const repository = new TransactionRepository(config);
47
+
48
+ const { data, isLoading, error, refetch } = useQuery({
49
+ queryKey: [...transactionQueryKeys.user(userId ?? ""), limit],
50
+ queryFn: async () => {
51
+ if (!userId) return [];
52
+
53
+ const result = await repository.getTransactions({
54
+ userId,
55
+ limit,
56
+ });
57
+
58
+ if (!result.success) {
59
+ throw new Error(result.error?.message || "Failed to fetch history");
60
+ }
61
+
62
+ return result.data ?? [];
63
+ },
64
+ enabled: enabled && !!userId,
65
+ staleTime: CACHE_CONFIG.staleTime,
66
+ gcTime: CACHE_CONFIG.gcTime,
67
+ });
68
+
69
+ const transactions = data ?? [];
70
+
71
+ if (__DEV__) {
72
+ console.log("[useTransactionHistory] State", {
73
+ userId,
74
+ enabled,
75
+ isLoading,
76
+ count: transactions.length,
77
+ });
78
+ }
79
+
80
+ return {
81
+ transactions,
82
+ isLoading,
83
+ error: error as Error | null,
84
+ refetch,
85
+ isEmpty: transactions.length === 0,
86
+ };
87
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * useWallet Hook
3
+ *
4
+ * Orchestration hook for wallet functionality.
5
+ * Combines balance, transactions, and purchase state.
6
+ */
7
+
8
+ import { useCallback, useMemo } from "react";
9
+ import {
10
+ useCredits,
11
+ type UseCreditsParams,
12
+ } from "../../../../presentation/hooks/useCredits";
13
+ import {
14
+ useTransactionHistory,
15
+ type UseTransactionHistoryParams,
16
+ } from "./useTransactionHistory";
17
+ import type { CreditLog } from "../../domain/types/transaction.types";
18
+
19
+ export interface UseWalletParams {
20
+ userId: string | undefined;
21
+ transactionConfig: UseTransactionHistoryParams["config"];
22
+ transactionLimit?: number;
23
+ enabled?: boolean;
24
+ }
25
+
26
+ export interface UseWalletResult {
27
+ balance: number;
28
+ textCredits: number;
29
+ imageCredits: number;
30
+ balanceLoading: boolean;
31
+ transactions: CreditLog[];
32
+ transactionsLoading: boolean;
33
+ hasCredits: boolean;
34
+ refetchBalance: () => void;
35
+ refetchTransactions: () => void;
36
+ refetchAll: () => void;
37
+ }
38
+
39
+ export function useWallet({
40
+ userId,
41
+ transactionConfig,
42
+ transactionLimit = 50,
43
+ enabled = true,
44
+ }: UseWalletParams): UseWalletResult {
45
+ const creditsParams: UseCreditsParams = {
46
+ userId,
47
+ enabled,
48
+ };
49
+
50
+ const transactionParams: UseTransactionHistoryParams = {
51
+ userId,
52
+ config: transactionConfig,
53
+ limit: transactionLimit,
54
+ enabled,
55
+ };
56
+
57
+ const {
58
+ credits,
59
+ isLoading: balanceLoading,
60
+ refetch: refetchBalance,
61
+ hasTextCredits,
62
+ hasImageCredits,
63
+ } = useCredits(creditsParams);
64
+
65
+ const {
66
+ transactions,
67
+ isLoading: transactionsLoading,
68
+ refetch: refetchTransactions,
69
+ } = useTransactionHistory(transactionParams);
70
+
71
+ const balance = useMemo(() => {
72
+ if (!credits) return 0;
73
+ return credits.textCredits + credits.imageCredits;
74
+ }, [credits]);
75
+
76
+ const refetchAll = useCallback(() => {
77
+ refetchBalance();
78
+ refetchTransactions();
79
+ }, [refetchBalance, refetchTransactions]);
80
+
81
+ return {
82
+ balance,
83
+ textCredits: credits?.textCredits ?? 0,
84
+ imageCredits: credits?.imageCredits ?? 0,
85
+ balanceLoading,
86
+ transactions,
87
+ transactionsLoading,
88
+ hasCredits: hasTextCredits || hasImageCredits,
89
+ refetchBalance,
90
+ refetchTransactions,
91
+ refetchAll,
92
+ };
93
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Wallet Screen
3
+ *
4
+ * Generic wallet screen composition.
5
+ * Props-driven for full customization across apps.
6
+ * No business logic - pure presentation.
7
+ */
8
+
9
+ import React from "react";
10
+ import { View, StyleSheet, ActivityIndicator, TouchableOpacity } from "react-native";
11
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
12
+ import {
13
+ useAppDesignTokens,
14
+ AtomicText,
15
+ AtomicIcon,
16
+ ScreenLayout,
17
+ } from "@umituz/react-native-design-system";
18
+ import {
19
+ BalanceCard,
20
+ type BalanceCardTranslations,
21
+ } from "../components/BalanceCard";
22
+ import {
23
+ TransactionList,
24
+ type TransactionListTranslations,
25
+ } from "../components/TransactionList";
26
+ import type { CreditLog } from "../../domain/types/transaction.types";
27
+
28
+ export interface WalletScreenTranslations
29
+ extends BalanceCardTranslations,
30
+ TransactionListTranslations {
31
+ screenTitle: string;
32
+ }
33
+
34
+ export interface WalletScreenConfig {
35
+ balance: number;
36
+ balanceLoading: boolean;
37
+ transactions: CreditLog[];
38
+ transactionsLoading: boolean;
39
+ translations: WalletScreenTranslations;
40
+ onBack?: () => void;
41
+ dateFormatter?: (timestamp: number) => string;
42
+ maxTransactionHeight?: number;
43
+ balanceIconName?: string;
44
+ footer?: React.ReactNode;
45
+ }
46
+
47
+ export interface WalletScreenProps {
48
+ config: WalletScreenConfig;
49
+ }
50
+
51
+ export const WalletScreen: React.FC<WalletScreenProps> = ({ config }) => {
52
+ const tokens = useAppDesignTokens();
53
+ const insets = useSafeAreaInsets();
54
+
55
+ const renderHeader = () => (
56
+ <View style={[styles.header, { paddingTop: insets.top + 12 }]}>
57
+ {config.onBack && (
58
+ <TouchableOpacity
59
+ onPress={config.onBack}
60
+ style={styles.backButton}
61
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
62
+ >
63
+ <AtomicIcon
64
+ name="ArrowLeft"
65
+ size="lg"
66
+ customColor={tokens.colors.textPrimary}
67
+ />
68
+ </TouchableOpacity>
69
+ )}
70
+ <AtomicText
71
+ type="titleLarge"
72
+ style={{ color: tokens.colors.textPrimary, fontWeight: "700" }}
73
+ >
74
+ {config.translations.screenTitle}
75
+ </AtomicText>
76
+ </View>
77
+ );
78
+
79
+ const renderBalance = () => {
80
+ if (config.balanceLoading) {
81
+ return (
82
+ <View style={styles.loadingContainer}>
83
+ <ActivityIndicator size="large" color={tokens.colors.primary} />
84
+ <AtomicText
85
+ type="bodyMedium"
86
+ style={[styles.loadingText, { color: tokens.colors.textSecondary }]}
87
+ >
88
+ {config.translations.loading}
89
+ </AtomicText>
90
+ </View>
91
+ );
92
+ }
93
+
94
+ return (
95
+ <BalanceCard
96
+ balance={config.balance}
97
+ translations={config.translations}
98
+ iconName={config.balanceIconName}
99
+ />
100
+ );
101
+ };
102
+
103
+ return (
104
+ <ScreenLayout
105
+ scrollable={true}
106
+ edges={["bottom"]}
107
+ backgroundColor={tokens.colors.backgroundPrimary}
108
+ contentContainerStyle={styles.content}
109
+ footer={config.footer}
110
+ >
111
+ {renderHeader()}
112
+ {renderBalance()}
113
+ <TransactionList
114
+ transactions={config.transactions}
115
+ loading={config.transactionsLoading}
116
+ translations={config.translations}
117
+ maxHeight={config.maxTransactionHeight}
118
+ dateFormatter={config.dateFormatter}
119
+ />
120
+ </ScreenLayout>
121
+ );
122
+ };
123
+
124
+ const styles = StyleSheet.create({
125
+ content: {
126
+ paddingBottom: 24,
127
+ },
128
+ header: {
129
+ flexDirection: "row",
130
+ alignItems: "center",
131
+ paddingHorizontal: 16,
132
+ paddingBottom: 12,
133
+ },
134
+ backButton: {
135
+ marginRight: 16,
136
+ },
137
+ loadingContainer: {
138
+ padding: 40,
139
+ alignItems: "center",
140
+ justifyContent: "center",
141
+ gap: 12,
142
+ },
143
+ loadingText: {
144
+ fontSize: 14,
145
+ fontWeight: "500",
146
+ },
147
+ });
package/src/index.ts CHANGED
@@ -6,9 +6,11 @@
6
6
  */
7
7
 
8
8
  // =============================================================================
9
- // DOMAIN LAYER - Errors
9
+ // WALLET DOMAIN (Complete)
10
10
  // =============================================================================
11
11
 
12
+ export * from "./domains/wallet";
13
+
12
14
  // =============================================================================
13
15
  // DOMAIN LAYER - Subscription Status
14
16
  // =============================================================================
@@ -231,6 +233,15 @@ export type {
231
233
 
232
234
  export { DEFAULT_CREDITS_CONFIG } from "./domain/entities/Credits";
233
235
 
236
+ // =============================================================================
237
+ // CREDITS SYSTEM - Errors
238
+ // =============================================================================
239
+
240
+ export { InsufficientCreditsError } from "./domain/errors/InsufficientCreditsError";
241
+
242
+ // CreditCost, Transaction types, Wallet types, Credit-cost types
243
+ // are now exported from "./domains/wallet"
244
+
234
245
  // =============================================================================
235
246
  // CREDITS SYSTEM - Repository
236
247
  // =============================================================================
@@ -240,6 +251,9 @@ export {
240
251
  createCreditsRepository,
241
252
  } from "./infrastructure/repositories/CreditsRepository";
242
253
 
254
+ // TransactionRepository and ProductMetadataService
255
+ // are now exported from "./domains/wallet"
256
+
243
257
  // =============================================================================
244
258
  // CREDITS SYSTEM - Configuration (Module-Level Provider)
245
259
  // =============================================================================
@@ -316,6 +330,9 @@ export {
316
330
 
317
331
  export { useDevTestCallbacks } from "./presentation/hooks/useDevTestCallbacks";
318
332
 
333
+ // Wallet hooks, components, and screens
334
+ // are now exported from "./domains/wallet"
335
+
319
336
  // =============================================================================
320
337
  // CREDITS SYSTEM - Utilities
321
338
  // =============================================================================
@@ -38,6 +38,8 @@ export interface UseCreditsResult {
38
38
  textCreditsPercent: number;
39
39
  imageCreditsPercent: number;
40
40
  refetch: () => void;
41
+ /** Check if user can afford a specific credit cost */
42
+ canAfford: (cost: number, type?: CreditType) => boolean;
41
43
  }
42
44
 
43
45
  export const useCredits = ({
@@ -86,6 +88,13 @@ export const useCredits = ({
86
88
  ? Math.round((credits.imageCredits / config.imageCreditLimit) * 100)
87
89
  : 0;
88
90
 
91
+ const canAfford = (cost: number, type: CreditType = "text"): boolean => {
92
+ if (!credits) return false;
93
+ return type === "text"
94
+ ? credits.textCredits >= cost
95
+ : credits.imageCredits >= cost;
96
+ };
97
+
89
98
  return {
90
99
  credits,
91
100
  isLoading,
@@ -95,6 +104,7 @@ export const useCredits = ({
95
104
  textCreditsPercent,
96
105
  imageCreditsPercent,
97
106
  refetch,
107
+ canAfford,
98
108
  };
99
109
  };
100
110