@umituz/react-native-subscription 2.27.114 → 2.27.116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/domains/credits/infrastructure/CreditsRepository.ts +16 -19
- package/src/domains/credits/utils/creditCalculations.ts +6 -11
- package/src/domains/subscription/application/SubscriptionSyncService.ts +3 -0
- package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +3 -0
- package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +1 -0
- package/src/domains/subscription/infrastructure/services/RevenueCatService.ts +1 -0
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +3 -0
- package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +4 -8
- package/src/domains/wallet/infrastructure/repositories/TransactionRepository.ts +18 -42
- package/src/domains/wallet/infrastructure/services/ProductMetadataService.ts +2 -6
- package/src/shared/infrastructure/SubscriptionEventBus.ts +1 -0
- package/src/shared/infrastructure/firestore/collectionUtils.ts +67 -0
- package/src/shared/infrastructure/firestore/index.ts +6 -0
- package/src/shared/infrastructure/firestore/resultUtils.ts +68 -0
- package/src/shared/infrastructure/index.ts +6 -0
- package/src/shared/presentation/hooks/index.ts +6 -0
- package/src/shared/presentation/hooks/useAsyncState.ts +72 -0
- package/src/shared/presentation/hooks/useServiceCall.ts +66 -0
- package/src/shared/types/CommonTypes.ts +6 -1
- package/src/shared/types/ReactTypes.ts +80 -0
- package/src/shared/utils/Result.ts +0 -16
- package/src/shared/utils/arrayUtils.core.ts +81 -0
- package/src/shared/utils/arrayUtils.query.ts +118 -0
- package/src/shared/utils/arrayUtils.transforms.ts +116 -0
- package/src/shared/utils/arrayUtils.ts +19 -0
- package/src/shared/utils/index.ts +14 -0
- package/src/shared/utils/numberUtils.aggregate.ts +35 -0
- package/src/shared/utils/numberUtils.core.ts +73 -0
- package/src/shared/utils/numberUtils.format.ts +42 -0
- package/src/shared/utils/numberUtils.math.ts +48 -0
- package/src/shared/utils/numberUtils.ts +9 -0
- package/src/shared/utils/stringUtils.case.ts +64 -0
- package/src/shared/utils/stringUtils.check.ts +65 -0
- package/src/shared/utils/stringUtils.format.ts +84 -0
- package/src/shared/utils/stringUtils.generate.ts +47 -0
- package/src/shared/utils/stringUtils.modify.ts +67 -0
- package/src/shared/utils/stringUtils.ts +10 -0
- package/src/shared/utils/validators.ts +187 -0
- package/src/utils/dateUtils.compare.ts +65 -0
- package/src/utils/dateUtils.core.ts +67 -0
- package/src/utils/dateUtils.format.ts +138 -0
- package/src/utils/dateUtils.math.ts +112 -0
- package/src/utils/dateUtils.ts +6 -28
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.116",
|
|
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",
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Optimized to use Design Patterns: Command, Observer, and Strategy.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import { BaseRepository
|
|
6
|
+
import { getDoc, setDoc, type Firestore } from "firebase/firestore";
|
|
7
|
+
import { BaseRepository } from "@umituz/react-native-firebase";
|
|
8
8
|
import type { CreditsConfig, CreditsResult, DeductCreditsResult } from "../core/Credits";
|
|
9
9
|
import type { UserCreditsDocumentRead, PurchaseSource } from "../core/UserCreditsDocument";
|
|
10
10
|
import { initializeCreditsTransaction } from "../application/CreditsInitializer";
|
|
@@ -13,7 +13,7 @@ import type { RevenueCatData } from "../../subscription/core/RevenueCatData";
|
|
|
13
13
|
import { DeductCreditsCommand } from "../application/DeductCreditsCommand";
|
|
14
14
|
import { CreditLimitCalculator } from "../application/CreditLimitCalculator";
|
|
15
15
|
import { PURCHASE_TYPE, type PurchaseType } from "../../subscription/core/SubscriptionConstants";
|
|
16
|
-
import {
|
|
16
|
+
import { requireFirestore, buildDocRef, type CollectionConfig } from "../../../shared/infrastructure/firestore";
|
|
17
17
|
|
|
18
18
|
export class CreditsRepository extends BaseRepository {
|
|
19
19
|
private deductCommand: DeductCreditsCommand;
|
|
@@ -23,19 +23,22 @@ export class CreditsRepository extends BaseRepository {
|
|
|
23
23
|
this.deductCommand = new DeductCreditsCommand((db, uid) => this.getRef(db, uid));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
private getCollectionConfig(): CollectionConfig {
|
|
27
|
+
return {
|
|
28
|
+
collectionName: "credits",
|
|
29
|
+
useUserSubcollection: this.config.useUserSubcollection,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
private getRef(db: Firestore, userId: string) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
: doc(db, this.config.collectionName, userId);
|
|
34
|
+
const config = this.getCollectionConfig();
|
|
35
|
+
return buildDocRef(db, userId, "balance", config);
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
async getCredits(userId: string): Promise<CreditsResult> {
|
|
33
|
-
const db =
|
|
34
|
-
if (!db) {
|
|
35
|
-
throw new Error("Firestore instance is not available");
|
|
36
|
-
}
|
|
37
|
-
|
|
39
|
+
const db = requireFirestore();
|
|
38
40
|
const snap = await getDoc(this.getRef(db, userId));
|
|
41
|
+
|
|
39
42
|
if (!snap.exists()) {
|
|
40
43
|
return { success: true, data: null, error: null };
|
|
41
44
|
}
|
|
@@ -52,11 +55,7 @@ export class CreditsRepository extends BaseRepository {
|
|
|
52
55
|
revenueCatData: RevenueCatData,
|
|
53
56
|
type: PurchaseType = PURCHASE_TYPE.INITIAL
|
|
54
57
|
): Promise<CreditsResult> {
|
|
55
|
-
const db =
|
|
56
|
-
if (!db) {
|
|
57
|
-
throw new Error("Firestore instance is not available");
|
|
58
|
-
}
|
|
59
|
-
|
|
58
|
+
const db = requireFirestore();
|
|
60
59
|
const creditLimit = CreditLimitCalculator.calculate(productId, this.config);
|
|
61
60
|
const cfg = { ...this.config, creditLimit };
|
|
62
61
|
|
|
@@ -98,9 +97,7 @@ export class CreditsRepository extends BaseRepository {
|
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
async syncExpiredStatus(userId: string): Promise<void> {
|
|
101
|
-
const db =
|
|
102
|
-
if (!db) throw new Error("Firestore instance is not available");
|
|
103
|
-
|
|
100
|
+
const db = requireFirestore();
|
|
104
101
|
const ref = this.getRef(db, userId);
|
|
105
102
|
await setDoc(ref, {
|
|
106
103
|
isPremium: false,
|
|
@@ -1,33 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Credit Calculation Utilities
|
|
3
3
|
* Centralized logic for credit mathematical operations
|
|
4
|
+
* Uses shared number utilities for consistency
|
|
4
5
|
*/
|
|
5
6
|
|
|
7
|
+
import { calculateCreditPercentage as calcPct, canAfford as canAffordCheck, calculateRemaining } from "../../../shared/utils/numberUtils";
|
|
8
|
+
|
|
6
9
|
export const calculateCreditPercentage = (
|
|
7
10
|
currentCredits: number | null | undefined,
|
|
8
11
|
creditLimit: number
|
|
9
12
|
): number => {
|
|
10
|
-
|
|
11
|
-
return 0;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const percent = Math.round((currentCredits / creditLimit) * 100);
|
|
15
|
-
return Math.min(Math.max(percent, 0), 100); // Clamp between 0-100
|
|
13
|
+
return calcPct(currentCredits, creditLimit);
|
|
16
14
|
};
|
|
17
15
|
|
|
18
16
|
export const canAffordCost = (
|
|
19
17
|
currentCredits: number | null | undefined,
|
|
20
18
|
cost: number
|
|
21
19
|
): boolean => {
|
|
22
|
-
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
return currentCredits >= cost;
|
|
20
|
+
return canAffordCheck(currentCredits, cost);
|
|
26
21
|
};
|
|
27
22
|
|
|
28
23
|
export const calculateRemainingCredits = (
|
|
29
24
|
currentCredits: number,
|
|
30
25
|
cost: number
|
|
31
26
|
): number => {
|
|
32
|
-
return
|
|
27
|
+
return calculateRemaining(currentCredits, cost);
|
|
33
28
|
};
|
|
@@ -31,6 +31,7 @@ export class SubscriptionSyncService {
|
|
|
31
31
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
32
32
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PURCHASE_COMPLETED, { userId, productId });
|
|
33
33
|
} catch {
|
|
34
|
+
// Ignored to prevent background sync errors from disrupting user experience
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -54,6 +55,7 @@ export class SubscriptionSyncService {
|
|
|
54
55
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
55
56
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.RENEWAL_DETECTED, { userId, productId });
|
|
56
57
|
} catch {
|
|
58
|
+
// Ignored to prevent background sync errors from disrupting user experience
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -121,6 +123,7 @@ export class SubscriptionSyncService {
|
|
|
121
123
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
122
124
|
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.PREMIUM_STATUS_CHANGED, { userId, isPremium });
|
|
123
125
|
} catch {
|
|
126
|
+
// Ignored to prevent background sync errors from disrupting user experience
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
129
|
}
|
|
@@ -66,6 +66,7 @@ export class CustomerInfoListenerManager {
|
|
|
66
66
|
customerInfo
|
|
67
67
|
);
|
|
68
68
|
} catch {
|
|
69
|
+
// Silently fail listener callbacks to prevent crashing the main listener
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -80,6 +81,7 @@ export class CustomerInfoListenerManager {
|
|
|
80
81
|
customerInfo
|
|
81
82
|
);
|
|
82
83
|
} catch {
|
|
84
|
+
// Silently fail listener callbacks to prevent crashing the main listener
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
|
|
@@ -91,6 +93,7 @@ export class CustomerInfoListenerManager {
|
|
|
91
93
|
try {
|
|
92
94
|
await syncPremiumStatus(config, this.currentUserId, customerInfo);
|
|
93
95
|
} catch {
|
|
96
|
+
// Silently fail listener callbacks to prevent crashing the main listener
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
99
|
};
|
|
@@ -36,6 +36,7 @@ export async function syncPremiumStatus(
|
|
|
36
36
|
await config.onPremiumStatusChanged(userId, false, undefined, undefined, undefined, undefined);
|
|
37
37
|
}
|
|
38
38
|
} catch {
|
|
39
|
+
// Silently fail callback notifications to prevent crashing the main flow
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -53,6 +54,7 @@ export async function notifyPurchaseCompleted(
|
|
|
53
54
|
try {
|
|
54
55
|
await config.onPurchaseCompleted(userId, productId, customerInfo, source);
|
|
55
56
|
} catch {
|
|
57
|
+
// Silently fail callback notifications to prevent crashing the main flow
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -69,5 +71,6 @@ export async function notifyRestoreCompleted(
|
|
|
69
71
|
try {
|
|
70
72
|
await config.onRestoreCompleted(userId, isPremium, customerInfo);
|
|
71
73
|
} catch {
|
|
74
|
+
// Silently fail callback notifications to prevent crashing the main flow
|
|
72
75
|
}
|
|
73
76
|
}
|
|
@@ -84,15 +84,11 @@ export const useAuthAwarePurchase = (
|
|
|
84
84
|
return false;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
authPurchaseStateManager.clearSavedPurchase();
|
|
91
|
-
}
|
|
92
|
-
return result;
|
|
93
|
-
} catch (error) {
|
|
94
|
-
throw error;
|
|
87
|
+
const result = await purchasePackage(saved.pkg);
|
|
88
|
+
if (result) {
|
|
89
|
+
authPurchaseStateManager.clearSavedPurchase();
|
|
95
90
|
}
|
|
91
|
+
return result;
|
|
96
92
|
}, [purchasePackage]);
|
|
97
93
|
|
|
98
94
|
return {
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
collection,
|
|
10
9
|
getDocs,
|
|
11
10
|
addDoc,
|
|
12
11
|
query,
|
|
@@ -14,10 +13,9 @@ import {
|
|
|
14
13
|
orderBy,
|
|
15
14
|
limit as firestoreLimit,
|
|
16
15
|
serverTimestamp,
|
|
17
|
-
type Firestore,
|
|
18
16
|
type QueryConstraint,
|
|
19
17
|
} from "firebase/firestore";
|
|
20
|
-
import { BaseRepository
|
|
18
|
+
import { BaseRepository } from "@umituz/react-native-firebase";
|
|
21
19
|
import type {
|
|
22
20
|
CreditLog,
|
|
23
21
|
TransactionRepositoryConfig,
|
|
@@ -26,6 +24,7 @@ import type {
|
|
|
26
24
|
TransactionReason,
|
|
27
25
|
} from "../../domain/types/transaction.types";
|
|
28
26
|
import { TransactionMapper } from "../../domain/mappers/TransactionMapper";
|
|
27
|
+
import { requireFirestore, buildCollectionRef, type CollectionConfig, mapErrorToResult } from "../../../../shared/infrastructure/firestore";
|
|
29
28
|
|
|
30
29
|
export class TransactionRepository extends BaseRepository {
|
|
31
30
|
private config: TransactionRepositoryConfig;
|
|
@@ -35,25 +34,23 @@ export class TransactionRepository extends BaseRepository {
|
|
|
35
34
|
this.config = config;
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
private getCollectionConfig(): CollectionConfig {
|
|
38
|
+
return {
|
|
39
|
+
collectionName: this.config.collectionName,
|
|
40
|
+
useUserSubcollection: this.config.useUserSubcollection ?? false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private getCollectionRef(db: any, userId: string) {
|
|
45
|
+
const config = this.getCollectionConfig();
|
|
46
|
+
return buildCollectionRef(db, userId, config);
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
async getTransactions(
|
|
46
50
|
options: TransactionQueryOptions
|
|
47
51
|
): Promise<TransactionResult> {
|
|
48
|
-
const db = getFirestore();
|
|
49
|
-
if (!db) {
|
|
50
|
-
return {
|
|
51
|
-
success: false,
|
|
52
|
-
error: { message: "Database not available", code: "DB_NOT_AVAILABLE" },
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
52
|
try {
|
|
53
|
+
const db = requireFirestore();
|
|
57
54
|
const colRef = this.getCollectionRef(db, options.userId);
|
|
58
55
|
const constraints: QueryConstraint[] = [];
|
|
59
56
|
|
|
@@ -67,20 +64,13 @@ export class TransactionRepository extends BaseRepository {
|
|
|
67
64
|
const q = query(colRef, ...constraints);
|
|
68
65
|
const snapshot = await getDocs(q);
|
|
69
66
|
|
|
70
|
-
const transactions: CreditLog[] = snapshot.docs.map((docSnap) =>
|
|
67
|
+
const transactions: CreditLog[] = snapshot.docs.map((docSnap) =>
|
|
71
68
|
TransactionMapper.toEntity(docSnap, options.userId)
|
|
72
69
|
);
|
|
73
70
|
|
|
74
71
|
return { success: true, data: transactions };
|
|
75
72
|
} catch (error) {
|
|
76
|
-
return
|
|
77
|
-
success: false,
|
|
78
|
-
error: {
|
|
79
|
-
message:
|
|
80
|
-
error instanceof Error ? error.message : "Failed to get logs",
|
|
81
|
-
code: "FETCH_FAILED",
|
|
82
|
-
},
|
|
83
|
-
};
|
|
73
|
+
return mapErrorToResult(error);
|
|
84
74
|
}
|
|
85
75
|
}
|
|
86
76
|
|
|
@@ -90,15 +80,8 @@ export class TransactionRepository extends BaseRepository {
|
|
|
90
80
|
reason: TransactionReason,
|
|
91
81
|
metadata?: Partial<CreditLog>
|
|
92
82
|
): Promise<TransactionResult<CreditLog>> {
|
|
93
|
-
const db = getFirestore();
|
|
94
|
-
if (!db) {
|
|
95
|
-
return {
|
|
96
|
-
success: false,
|
|
97
|
-
error: { message: "Database not available", code: "DB_NOT_AVAILABLE" },
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
83
|
try {
|
|
84
|
+
const db = requireFirestore();
|
|
102
85
|
const colRef = this.getCollectionRef(db, userId);
|
|
103
86
|
const docData = {
|
|
104
87
|
...TransactionMapper.toFirestore(userId, change, reason, metadata),
|
|
@@ -118,15 +101,8 @@ export class TransactionRepository extends BaseRepository {
|
|
|
118
101
|
createdAt: Date.now(),
|
|
119
102
|
},
|
|
120
103
|
};
|
|
121
|
-
} catch {
|
|
122
|
-
return
|
|
123
|
-
success: false,
|
|
124
|
-
error: {
|
|
125
|
-
message:
|
|
126
|
-
error instanceof Error ? error.message : "Failed to add log",
|
|
127
|
-
code: "ADD_FAILED",
|
|
128
|
-
},
|
|
129
|
-
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return mapErrorToResult<CreditLog>(error);
|
|
130
106
|
}
|
|
131
107
|
}
|
|
132
108
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { collection, getDocs, orderBy, query } from "firebase/firestore";
|
|
9
|
-
import {
|
|
9
|
+
import { requireFirestore } from "../../../../shared/infrastructure";
|
|
10
10
|
import type {
|
|
11
11
|
ProductMetadata,
|
|
12
12
|
ProductMetadataConfig,
|
|
@@ -35,11 +35,7 @@ export class ProductMetadataService {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
private async fetchFromFirebase(): Promise<ProductMetadata[]> {
|
|
38
|
-
const db =
|
|
39
|
-
if (!db) {
|
|
40
|
-
throw new Error("Firestore not initialized");
|
|
41
|
-
}
|
|
42
|
-
|
|
38
|
+
const db = requireFirestore();
|
|
43
39
|
const colRef = collection(db, this.config.collectionName);
|
|
44
40
|
const q = query(colRef, orderBy("order", "asc"));
|
|
45
41
|
const snapshot = await getDocs(q);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firestore Collection Utilities
|
|
3
|
+
* Shared utilities for building Firestore collection and document references
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
collection,
|
|
8
|
+
doc,
|
|
9
|
+
type CollectionReference,
|
|
10
|
+
type DocumentReference,
|
|
11
|
+
type Firestore,
|
|
12
|
+
} from "firebase/firestore";
|
|
13
|
+
import { getFirestore } from "@umituz/react-native-firebase";
|
|
14
|
+
|
|
15
|
+
export interface CollectionConfig {
|
|
16
|
+
collectionName: string;
|
|
17
|
+
useUserSubcollection: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build a collection reference based on configuration
|
|
22
|
+
* Supports both root collections and user subcollections
|
|
23
|
+
*/
|
|
24
|
+
export function buildCollectionRef(
|
|
25
|
+
db: Firestore,
|
|
26
|
+
userId: string,
|
|
27
|
+
config: CollectionConfig
|
|
28
|
+
): CollectionReference {
|
|
29
|
+
if (config.useUserSubcollection) {
|
|
30
|
+
return collection(db, "users", userId, config.collectionName);
|
|
31
|
+
}
|
|
32
|
+
return collection(db, config.collectionName);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build a document reference based on configuration
|
|
37
|
+
* Supports both root collections and user subcollections
|
|
38
|
+
*/
|
|
39
|
+
export function buildDocRef(
|
|
40
|
+
db: Firestore,
|
|
41
|
+
userId: string,
|
|
42
|
+
docId: string,
|
|
43
|
+
config: CollectionConfig
|
|
44
|
+
): DocumentReference {
|
|
45
|
+
if (config.useUserSubcollection) {
|
|
46
|
+
return doc(db, "users", userId, config.collectionName, docId);
|
|
47
|
+
}
|
|
48
|
+
return doc(db, config.collectionName, docId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get Firestore instance or throw error
|
|
53
|
+
*/
|
|
54
|
+
export function requireFirestore(): Firestore {
|
|
55
|
+
const db = getFirestore();
|
|
56
|
+
if (!db) {
|
|
57
|
+
throw new Error("Firestore instance is not available");
|
|
58
|
+
}
|
|
59
|
+
return db;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Safe check for Firestore availability
|
|
64
|
+
*/
|
|
65
|
+
export function isFirestoreAvailable(): boolean {
|
|
66
|
+
return getFirestore() !== null;
|
|
67
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result Utilities
|
|
3
|
+
* Shared helpers for working with Result pattern
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Result } from "../../utils/Result";
|
|
7
|
+
import { failure, success } from "../../utils/Result";
|
|
8
|
+
|
|
9
|
+
export interface ApiError {
|
|
10
|
+
message: string;
|
|
11
|
+
code: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a standard error result
|
|
16
|
+
*/
|
|
17
|
+
export function createErrorResult(
|
|
18
|
+
message: string,
|
|
19
|
+
code: string = "UNKNOWN_ERROR"
|
|
20
|
+
): Result<never, ApiError> {
|
|
21
|
+
return failure({ message, code });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a database unavailable error result
|
|
26
|
+
*/
|
|
27
|
+
export function createDbUnavailableResult<T>(): Result<T, ApiError> {
|
|
28
|
+
return createErrorResult("Database not available", "DB_NOT_AVAILABLE");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Map Error to ApiError result
|
|
33
|
+
*/
|
|
34
|
+
export function mapErrorToResult<T>(error: unknown): Result<T, ApiError> {
|
|
35
|
+
const message = error instanceof Error ? error.message : "An unknown error occurred";
|
|
36
|
+
const code = error instanceof Error && "code" in error ? String(error.code) : "UNKNOWN_ERROR";
|
|
37
|
+
return createErrorResult(message, code);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Execute async function and return Result
|
|
42
|
+
*/
|
|
43
|
+
export async function executeAsResult<T>(
|
|
44
|
+
fn: () => Promise<T>
|
|
45
|
+
): Promise<Result<T, ApiError>> {
|
|
46
|
+
try {
|
|
47
|
+
const data = await fn();
|
|
48
|
+
return success(data);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return mapErrorToResult<T>(error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate database availability before executing
|
|
56
|
+
*/
|
|
57
|
+
export async function withDbCheck<T>(
|
|
58
|
+
fn: (db: any) => Promise<T>
|
|
59
|
+
): Promise<Result<T, ApiError>> {
|
|
60
|
+
const { requireFirestore } = require("./collectionUtils");
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const db = requireFirestore();
|
|
64
|
+
return await executeAsResult(() => fn(db));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
return mapErrorToResult<T>(error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAsyncState Hook
|
|
3
|
+
* Shared hook for managing async operation states
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback } from "react";
|
|
7
|
+
|
|
8
|
+
export type AsyncStatus = "idle" | "loading" | "success" | "error";
|
|
9
|
+
|
|
10
|
+
export interface AsyncState<T> {
|
|
11
|
+
data: T | null;
|
|
12
|
+
status: AsyncStatus;
|
|
13
|
+
error: Error | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UseAsyncStateOptions<T> {
|
|
17
|
+
initialData?: T | null;
|
|
18
|
+
onSuccess?: (data: T) => void;
|
|
19
|
+
onError?: (error: Error) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UseAsyncStateReturn<T> {
|
|
23
|
+
data: T | null;
|
|
24
|
+
status: AsyncStatus;
|
|
25
|
+
error: Error | null;
|
|
26
|
+
isLoading: boolean;
|
|
27
|
+
isSuccess: boolean;
|
|
28
|
+
isError: boolean;
|
|
29
|
+
isIdle: boolean;
|
|
30
|
+
setData: (data: T | null) => void;
|
|
31
|
+
setError: (error: Error | null) => void;
|
|
32
|
+
reset: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useAsyncState<T>(
|
|
36
|
+
options: UseAsyncStateOptions<T> = {}
|
|
37
|
+
): UseAsyncStateReturn<T> {
|
|
38
|
+
const { initialData = null, onSuccess, onError } = options;
|
|
39
|
+
|
|
40
|
+
const [state, setState] = useState<AsyncState<T>>({
|
|
41
|
+
data: initialData,
|
|
42
|
+
status: initialData ? "success" : "idle",
|
|
43
|
+
error: null,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const setData = useCallback((data: T | null) => {
|
|
47
|
+
setState({ data, status: data ? "success" : "idle", error: null });
|
|
48
|
+
if (data) onSuccess?.(data);
|
|
49
|
+
}, [onSuccess]);
|
|
50
|
+
|
|
51
|
+
const setError = useCallback((error: Error | null) => {
|
|
52
|
+
setState((prev) => ({ ...prev, status: "error", error }));
|
|
53
|
+
if (error) onError?.(error);
|
|
54
|
+
}, [onError]);
|
|
55
|
+
|
|
56
|
+
const reset = useCallback(() => {
|
|
57
|
+
setState({ data: initialData, status: initialData ? "success" : "idle", error: null });
|
|
58
|
+
}, [initialData]);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
data: state.data,
|
|
62
|
+
status: state.status,
|
|
63
|
+
error: state.error,
|
|
64
|
+
isLoading: state.status === "loading",
|
|
65
|
+
isSuccess: state.status === "success",
|
|
66
|
+
isError: state.status === "error",
|
|
67
|
+
isIdle: state.status === "idle",
|
|
68
|
+
setData,
|
|
69
|
+
setError,
|
|
70
|
+
reset,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useServiceCall Hook
|
|
3
|
+
* Shared hook for handling service calls with loading, error, and success states
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback } from "react";
|
|
7
|
+
|
|
8
|
+
export interface ServiceCallState<T> {
|
|
9
|
+
data: T | null;
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
error: Error | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UseServiceCallOptions<T> {
|
|
15
|
+
onSuccess?: (data: T) => void;
|
|
16
|
+
onError?: (error: Error) => void;
|
|
17
|
+
onComplete?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ServiceCallResult<T> {
|
|
21
|
+
data: T | null;
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
error: Error | null;
|
|
24
|
+
execute: () => Promise<void>;
|
|
25
|
+
reset: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useServiceCall<T>(
|
|
29
|
+
serviceFn: () => Promise<T>,
|
|
30
|
+
options: UseServiceCallOptions<T> = {}
|
|
31
|
+
): ServiceCallResult<T> {
|
|
32
|
+
const { onSuccess, onError, onComplete } = options;
|
|
33
|
+
const [state, setState] = useState<ServiceCallState<T>>({
|
|
34
|
+
data: null,
|
|
35
|
+
isLoading: false,
|
|
36
|
+
error: null,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const execute = useCallback(async () => {
|
|
40
|
+
setState({ data: null, isLoading: true, error: null });
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const data = await serviceFn();
|
|
44
|
+
setState({ data, isLoading: false, error: null });
|
|
45
|
+
onSuccess?.(data);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const errorObj = error instanceof Error ? error : new Error("Service call failed");
|
|
48
|
+
setState({ data: null, isLoading: false, error: errorObj });
|
|
49
|
+
onError?.(errorObj);
|
|
50
|
+
} finally {
|
|
51
|
+
onComplete?.();
|
|
52
|
+
}
|
|
53
|
+
}, [serviceFn, onSuccess, onError, onComplete]);
|
|
54
|
+
|
|
55
|
+
const reset = useCallback(() => {
|
|
56
|
+
setState({ data: null, isLoading: false, error: null });
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
data: state.data,
|
|
61
|
+
isLoading: state.isLoading,
|
|
62
|
+
error: state.error,
|
|
63
|
+
execute,
|
|
64
|
+
reset,
|
|
65
|
+
};
|
|
66
|
+
}
|