@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.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/infrastructure/CreditsRepository.ts +16 -19
  3. package/src/domains/credits/utils/creditCalculations.ts +6 -11
  4. package/src/domains/subscription/application/SubscriptionSyncService.ts +3 -0
  5. package/src/domains/subscription/infrastructure/services/CustomerInfoListenerManager.ts +3 -0
  6. package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +1 -0
  7. package/src/domains/subscription/infrastructure/services/RevenueCatService.ts +1 -0
  8. package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +3 -0
  9. package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +4 -8
  10. package/src/domains/wallet/infrastructure/repositories/TransactionRepository.ts +18 -42
  11. package/src/domains/wallet/infrastructure/services/ProductMetadataService.ts +2 -6
  12. package/src/shared/infrastructure/SubscriptionEventBus.ts +1 -0
  13. package/src/shared/infrastructure/firestore/collectionUtils.ts +67 -0
  14. package/src/shared/infrastructure/firestore/index.ts +6 -0
  15. package/src/shared/infrastructure/firestore/resultUtils.ts +68 -0
  16. package/src/shared/infrastructure/index.ts +6 -0
  17. package/src/shared/presentation/hooks/index.ts +6 -0
  18. package/src/shared/presentation/hooks/useAsyncState.ts +72 -0
  19. package/src/shared/presentation/hooks/useServiceCall.ts +66 -0
  20. package/src/shared/types/CommonTypes.ts +6 -1
  21. package/src/shared/types/ReactTypes.ts +80 -0
  22. package/src/shared/utils/Result.ts +0 -16
  23. package/src/shared/utils/arrayUtils.core.ts +81 -0
  24. package/src/shared/utils/arrayUtils.query.ts +118 -0
  25. package/src/shared/utils/arrayUtils.transforms.ts +116 -0
  26. package/src/shared/utils/arrayUtils.ts +19 -0
  27. package/src/shared/utils/index.ts +14 -0
  28. package/src/shared/utils/numberUtils.aggregate.ts +35 -0
  29. package/src/shared/utils/numberUtils.core.ts +73 -0
  30. package/src/shared/utils/numberUtils.format.ts +42 -0
  31. package/src/shared/utils/numberUtils.math.ts +48 -0
  32. package/src/shared/utils/numberUtils.ts +9 -0
  33. package/src/shared/utils/stringUtils.case.ts +64 -0
  34. package/src/shared/utils/stringUtils.check.ts +65 -0
  35. package/src/shared/utils/stringUtils.format.ts +84 -0
  36. package/src/shared/utils/stringUtils.generate.ts +47 -0
  37. package/src/shared/utils/stringUtils.modify.ts +67 -0
  38. package/src/shared/utils/stringUtils.ts +10 -0
  39. package/src/shared/utils/validators.ts +187 -0
  40. package/src/utils/dateUtils.compare.ts +65 -0
  41. package/src/utils/dateUtils.core.ts +67 -0
  42. package/src/utils/dateUtils.format.ts +138 -0
  43. package/src/utils/dateUtils.math.ts +112 -0
  44. 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.114",
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 { doc, getDoc, type Firestore } from "firebase/firestore";
7
- import { BaseRepository, getFirestore } from "@umituz/react-native-firebase";
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 { setDoc } from "firebase/firestore";
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
- return this.config.useUserSubcollection
28
- ? doc(db, "users", userId, "credits", "balance")
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 = getFirestore();
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 = getFirestore();
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 = getFirestore();
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
- if (currentCredits === null || currentCredits === undefined || creditLimit <= 0) {
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
- if (currentCredits === null || currentCredits === undefined) {
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 Math.max(0, currentCredits - cost);
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
  };
@@ -31,6 +31,7 @@ function configureLogHandler(): void {
31
31
  });
32
32
  configurationState.isLogHandlerConfigured = true;
33
33
  } catch {
34
+ // Failing to set log handler should not block initialization
34
35
  }
35
36
  }
36
37
 
@@ -109,6 +109,7 @@ export class RevenueCatService implements IRevenueCatService {
109
109
  await Purchases.logOut();
110
110
  this.stateManager.setInitialized(false);
111
111
  } catch {
112
+ // Silently fail during logout to allow cleanup to complete
112
113
  }
113
114
  }
114
115
  }
@@ -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
- try {
88
- const result = await purchasePackage(saved.pkg);
89
- if (result) {
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, getFirestore } from "@umituz/react-native-firebase";
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 getCollectionRef(db: Firestore, userId: string) {
39
- if (this.config.useUserSubcollection) {
40
- return collection(db, "users", userId, this.config.collectionName);
41
- }
42
- return collection(db, this.config.collectionName);
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 { getFirestore } from "@umituz/react-native-firebase";
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 = getFirestore();
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);
@@ -43,6 +43,7 @@ export class SubscriptionEventBus {
43
43
  try {
44
44
  callback(data);
45
45
  } catch {
46
+ // Prevent one faulty listener from breaking other listeners
46
47
  }
47
48
  });
48
49
  }
@@ -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,6 @@
1
+ /**
2
+ * Firestore Infrastructure Utilities
3
+ */
4
+
5
+ export * from "./collectionUtils";
6
+ export * from "./resultUtils";
@@ -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,6 @@
1
+ /**
2
+ * Shared Infrastructure
3
+ */
4
+
5
+ export * from "./SubscriptionEventBus";
6
+ export * from "./firestore";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shared Presentation Hooks
3
+ */
4
+
5
+ export * from "./useAsyncState";
6
+ export * from "./useServiceCall";
@@ -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
+ }