@umituz/react-native-subscription 2.14.82 → 2.14.84

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.
@@ -1,8 +1,6 @@
1
1
  /**
2
2
  * useDeductCredit Hook
3
- *
4
3
  * TanStack Query mutation hook for deducting credits.
5
- * Generic and reusable - uses module-level repository.
6
4
  */
7
5
 
8
6
  import { useCallback } from "react";
@@ -17,9 +15,7 @@ export interface UseDeductCreditParams {
17
15
  }
18
16
 
19
17
  export interface UseDeductCreditResult {
20
- /** Deduct a single credit */
21
18
  deductCredit: (creditType: CreditType) => Promise<boolean>;
22
- /** Deduct multiple credits (loops internally) */
23
19
  deductCredits: (cost: number, creditType?: CreditType) => Promise<boolean>;
24
20
  isDeducting: boolean;
25
21
  }
@@ -33,169 +29,47 @@ export const useDeductCredit = ({
33
29
 
34
30
  const mutation = useMutation({
35
31
  mutationFn: async (creditType: CreditType) => {
36
- if (!userId) {
37
- throw new Error("User not authenticated");
38
- }
32
+ if (!userId) throw new Error("User not authenticated");
39
33
  return repository.deductCredit(userId, creditType);
40
34
  },
41
35
  onMutate: async (creditType: CreditType) => {
42
36
  if (!userId) return;
43
-
44
- await queryClient.cancelQueries({
45
- queryKey: creditsQueryKeys.user(userId),
37
+ await queryClient.cancelQueries({ queryKey: creditsQueryKeys.user(userId) });
38
+ const previousCredits = queryClient.getQueryData<UserCredits>(creditsQueryKeys.user(userId));
39
+ queryClient.setQueryData<UserCredits | null>(creditsQueryKeys.user(userId), (old) => {
40
+ if (!old) return old;
41
+ const field = creditType === "text" ? "textCredits" : "imageCredits";
42
+ return { ...old, [field]: Math.max(0, old[field] - 1), lastUpdatedAt: new Date() };
46
43
  });
47
-
48
- const previousCredits = queryClient.getQueryData<UserCredits>(
49
- creditsQueryKeys.user(userId)
50
- );
51
-
52
- queryClient.setQueryData<UserCredits | null>(
53
- creditsQueryKeys.user(userId),
54
- (old) => {
55
- if (!old) return old;
56
- const fieldName =
57
- creditType === "text" ? "textCredits" : "imageCredits";
58
- return {
59
- ...old,
60
- [fieldName]: Math.max(0, old[fieldName] - 1),
61
- lastUpdatedAt: new Date(),
62
- };
63
- }
64
- );
65
-
66
44
  return { previousCredits };
67
45
  },
68
- onError: (_err, _creditType, context) => {
46
+ onError: (_err, _type, context) => {
69
47
  if (userId && context?.previousCredits) {
70
- queryClient.setQueryData(
71
- creditsQueryKeys.user(userId),
72
- context.previousCredits
73
- );
48
+ queryClient.setQueryData(creditsQueryKeys.user(userId), context.previousCredits);
74
49
  }
75
50
  },
76
51
  onSettled: () => {
77
- if (userId) {
78
- queryClient.invalidateQueries({
79
- queryKey: creditsQueryKeys.user(userId),
80
- });
81
- }
52
+ if (userId) queryClient.invalidateQueries({ queryKey: creditsQueryKeys.user(userId) });
82
53
  },
83
54
  });
84
55
 
85
- const deductCredit = useCallback(async (creditType: CreditType): Promise<boolean> => {
56
+ const deductCredit = useCallback(async (type: CreditType): Promise<boolean> => {
86
57
  try {
87
- const result = await mutation.mutateAsync(creditType);
88
-
89
- if (!result.success) {
90
- if (result.error?.code === "CREDITS_EXHAUSTED") {
91
- onCreditsExhausted?.();
92
- }
58
+ const res = await mutation.mutateAsync(type);
59
+ if (!res.success) {
60
+ if (res.error?.code === "CREDITS_EXHAUSTED") onCreditsExhausted?.();
93
61
  return false;
94
62
  }
95
-
96
63
  return true;
97
- } catch {
98
- return false;
99
- }
64
+ } catch { return false; }
100
65
  }, [mutation, onCreditsExhausted]);
101
66
 
102
- /**
103
- * Deduct multiple credits at once
104
- * Loops internally to deduct `cost` credits of the specified type
105
- */
106
- const deductCredits = useCallback(
107
- async (cost: number, creditType: CreditType = "image"): Promise<boolean> => {
108
- for (let i = 0; i < cost; i++) {
109
- const success = await deductCredit(creditType);
110
- if (!success) return false;
111
- }
112
- return true;
113
- },
114
- [deductCredit],
115
- );
116
-
117
- return {
118
- deductCredit,
119
- deductCredits,
120
- isDeducting: mutation.isPending,
121
- };
122
- };
123
-
124
- export interface UseInitializeCreditsParams {
125
- userId: string | undefined;
126
- }
127
-
128
- export interface InitializeCreditsOptions {
129
- purchaseId?: string;
130
- productId?: string;
131
- }
132
-
133
- export interface UseInitializeCreditsResult {
134
- initializeCredits: (options?: InitializeCreditsOptions) => Promise<boolean>;
135
- isInitializing: boolean;
136
- }
137
-
138
- export const useInitializeCredits = ({
139
- userId,
140
- }: UseInitializeCreditsParams): UseInitializeCreditsResult => {
141
- const repository = getCreditsRepository();
142
- const queryClient = useQueryClient();
143
-
144
- const mutation = useMutation({
145
- mutationFn: async (options?: InitializeCreditsOptions) => {
146
- if (!userId) {
147
- throw new Error("User not authenticated");
148
- }
149
-
150
- if (__DEV__) {
151
- console.log("[useInitializeCredits] Initializing credits:", {
152
- userId,
153
- purchaseId: options?.purchaseId,
154
- productId: options?.productId,
155
- });
156
- }
157
-
158
- return repository.initializeCredits(
159
- userId,
160
- options?.purchaseId,
161
- options?.productId
162
- );
163
- },
164
- onSuccess: (result) => {
165
- if (userId && result.success && result.data) {
166
- if (__DEV__) {
167
- console.log("[useInitializeCredits] Success, updating cache:", result.data);
168
- }
169
-
170
- // Set the data immediately for optimistic UI
171
- queryClient.setQueryData(creditsQueryKeys.user(userId), result.data);
172
- // Also invalidate to ensure all subscribers get the update
173
- queryClient.invalidateQueries({
174
- queryKey: creditsQueryKeys.user(userId),
175
- });
176
- }
177
- },
178
- onError: (error) => {
179
- if (__DEV__) {
180
- console.error("[useInitializeCredits] Error:", error);
181
- }
182
- },
183
- });
184
-
185
- const initializeCredits = useCallback(
186
- async (options?: InitializeCreditsOptions): Promise<boolean> => {
187
- try {
188
- const result = await mutation.mutateAsync(options);
189
- return result.success;
190
- } catch {
191
- return false;
192
- }
193
- },
194
- [mutation]
195
- );
67
+ const deductCredits = useCallback(async (cost: number, type: CreditType = "image"): Promise<boolean> => {
68
+ for (let i = 0; i < cost; i++) {
69
+ if (!(await deductCredit(type))) return false;
70
+ }
71
+ return true;
72
+ }, [deductCredit]);
196
73
 
197
- return {
198
- initializeCredits,
199
- isInitializing: mutation.isPending,
200
- };
74
+ return { deductCredit, deductCredits, isDeducting: mutation.isPending };
201
75
  };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * useInitializeCredits Hook
3
+ * TanStack Query mutation hook for initializing credits after purchase.
4
+ */
5
+
6
+ import { useCallback } from "react";
7
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
8
+ import { getCreditsRepository } from "../../infrastructure/repositories/CreditsRepositoryProvider";
9
+ import { creditsQueryKeys } from "./useCredits";
10
+
11
+ declare const __DEV__: boolean;
12
+
13
+ export interface UseInitializeCreditsParams {
14
+ userId: string | undefined;
15
+ }
16
+
17
+ export interface InitializeCreditsOptions {
18
+ purchaseId?: string;
19
+ productId?: string;
20
+ }
21
+
22
+ export interface UseInitializeCreditsResult {
23
+ initializeCredits: (options?: InitializeCreditsOptions) => Promise<boolean>;
24
+ isInitializing: boolean;
25
+ }
26
+
27
+ export const useInitializeCredits = ({
28
+ userId,
29
+ }: UseInitializeCreditsParams): UseInitializeCreditsResult => {
30
+ const repository = getCreditsRepository();
31
+ const queryClient = useQueryClient();
32
+
33
+ const mutation = useMutation({
34
+ mutationFn: async (options?: InitializeCreditsOptions) => {
35
+ if (!userId) throw new Error("User not authenticated");
36
+ if (__DEV__) console.log("[useInitializeCredits] Initializing:", { userId, ...options });
37
+ return repository.initializeCredits(userId, options?.purchaseId, options?.productId);
38
+ },
39
+ onSuccess: (result) => {
40
+ if (userId && result.success && result.data) {
41
+ if (__DEV__) console.log("[useInitializeCredits] Success:", result.data);
42
+ queryClient.setQueryData(creditsQueryKeys.user(userId), result.data);
43
+ queryClient.invalidateQueries({ queryKey: creditsQueryKeys.user(userId) });
44
+ }
45
+ },
46
+ onError: (error) => { if (__DEV__) console.error("[useInitializeCredits] Error:", error); },
47
+ });
48
+
49
+ const initializeCredits = useCallback(async (opts?: InitializeCreditsOptions): Promise<boolean> => {
50
+ try {
51
+ const res = await mutation.mutateAsync(opts);
52
+ return res.success;
53
+ } catch { return false; }
54
+ }, [mutation]);
55
+
56
+ return { initializeCredits, isInitializing: mutation.isPending };
57
+ };
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { useEffect, useCallback } from "react";
9
9
  import { useCredits, type UseCreditsResult } from "./useCredits";
10
- import { useInitializeCredits } from "./useDeductCredit";
10
+ import { useInitializeCredits } from "./useInitializeCredits";
11
11
 
12
12
  export interface UsePremiumWithCreditsParams {
13
13
  userId: string | undefined;
@@ -4,9 +4,13 @@
4
4
  */
5
5
 
6
6
  import { useState, useCallback } from 'react';
7
- import { getSubscriptionService } from '../../infrastructure/services/SubscriptionService';
8
7
  import type { SubscriptionStatus } from '../../domain/entities/SubscriptionStatus';
9
8
  import { isSubscriptionValid } from '../../domain/entities/SubscriptionStatus';
9
+ import {
10
+ checkSubscriptionService,
11
+ validateUserId,
12
+ executeSubscriptionOperation,
13
+ } from './useSubscription.utils';
10
14
 
11
15
  export interface UseSubscriptionResult {
12
16
  /** Current subscription status */
@@ -45,120 +49,96 @@ export function useSubscription(): UseSubscriptionResult {
45
49
  const [error, setError] = useState<string | null>(null);
46
50
 
47
51
  const loadStatus = useCallback(async (userId: string) => {
48
- if (!userId) {
49
- setError('User ID is required');
52
+ const validationError = validateUserId(userId);
53
+ if (validationError) {
54
+ setError(validationError);
50
55
  return;
51
56
  }
52
57
 
53
- const service = getSubscriptionService();
54
- if (!service) {
55
- setError('Subscription service is not initialized');
58
+ const serviceCheck = checkSubscriptionService();
59
+ if (!serviceCheck.success) {
60
+ setError(serviceCheck.error || "Service error");
56
61
  return;
57
62
  }
58
63
 
59
- setLoading(true);
60
- setError(null);
61
-
62
- try {
63
- const subscriptionStatus = await service.getSubscriptionStatus(userId);
64
- setStatus(subscriptionStatus);
65
- } catch (err) {
66
- const errorMessage =
67
- err instanceof Error ? err.message : 'Failed to load subscription status';
68
- setError(errorMessage);
69
- } finally {
70
- setLoading(false);
71
- }
64
+ await executeSubscriptionOperation(
65
+ () => serviceCheck.service!.getSubscriptionStatus(userId),
66
+ setLoading,
67
+ setError,
68
+ (result) => setStatus(result)
69
+ );
72
70
  }, []);
73
71
 
74
72
  const refreshStatus = useCallback(async (userId: string) => {
75
- if (!userId) {
76
- setError('User ID is required');
73
+ const validationError = validateUserId(userId);
74
+ if (validationError) {
75
+ setError(validationError);
77
76
  return;
78
77
  }
79
78
 
80
- const service = getSubscriptionService();
81
- if (!service) {
82
- setError('Subscription service is not initialized');
79
+ const serviceCheck = checkSubscriptionService();
80
+ if (!serviceCheck.success) {
81
+ setError(serviceCheck.error || "Service error");
83
82
  return;
84
83
  }
85
84
 
86
- setLoading(true);
87
- setError(null);
88
-
89
- try {
90
- const subscriptionStatus = await service.getSubscriptionStatus(userId);
91
- setStatus(subscriptionStatus);
92
- } catch (err) {
93
- const errorMessage =
94
- err instanceof Error ? err.message : 'Failed to refresh subscription status';
95
- setError(errorMessage);
96
- } finally {
97
- setLoading(false);
98
- }
85
+ await executeSubscriptionOperation(
86
+ () => serviceCheck.service!.getSubscriptionStatus(userId),
87
+ setLoading,
88
+ setError,
89
+ (result) => setStatus(result)
90
+ );
99
91
  }, []);
100
92
 
101
93
  const activateSubscription = useCallback(
102
94
  async (userId: string, productId: string, expiresAt: string | null) => {
103
- if (!userId || !productId) {
104
- setError('User ID and Product ID are required');
95
+ const validationError = validateUserId(userId);
96
+ if (validationError) {
97
+ setError(validationError);
105
98
  return;
106
99
  }
107
100
 
108
- const service = getSubscriptionService();
109
- if (!service) {
110
- setError('Subscription service is not initialized');
101
+ if (!productId) {
102
+ setError("Product ID is required");
111
103
  return;
112
104
  }
113
105
 
114
- setLoading(true);
115
- setError(null);
116
-
117
- try {
118
- const updatedStatus = await service.activateSubscription(
119
- userId,
120
- productId,
121
- expiresAt,
122
- );
123
- setStatus(updatedStatus);
124
- } catch (err) {
125
- const errorMessage =
126
- err instanceof Error ? err.message : 'Failed to activate subscription';
127
- setError(errorMessage);
128
- throw err;
129
- } finally {
130
- setLoading(false);
106
+ const serviceCheck = checkSubscriptionService();
107
+ if (!serviceCheck.success) {
108
+ setError(serviceCheck.error || "Service error");
109
+ return;
131
110
  }
111
+
112
+ await executeSubscriptionOperation(
113
+ () =>
114
+ serviceCheck.service!.activateSubscription(userId, productId, expiresAt),
115
+ setLoading,
116
+ setError,
117
+ (result) => setStatus(result)
118
+ );
132
119
  },
133
- [],
120
+ []
134
121
  );
135
122
 
136
123
  const deactivateSubscription = useCallback(async (userId: string) => {
137
- if (!userId) {
138
- setError('User ID is required');
124
+ const validationError = validateUserId(userId);
125
+ if (validationError) {
126
+ setError(validationError);
139
127
  return;
140
128
  }
141
129
 
142
- const service = getSubscriptionService();
143
- if (!service) {
144
- setError('Subscription service is not initialized');
130
+ const serviceCheck = checkSubscriptionService();
131
+ if (!serviceCheck.success) {
132
+ setError(serviceCheck.error || "Service error");
145
133
  return;
146
134
  }
147
135
 
148
- setLoading(true);
149
- setError(null);
150
-
151
- try {
152
- const updatedStatus = await service.deactivateSubscription(userId);
153
- setStatus(updatedStatus);
154
- } catch (err) {
155
- const errorMessage =
156
- err instanceof Error ? err.message : 'Failed to deactivate subscription';
157
- setError(errorMessage);
158
- throw err;
159
- } finally {
160
- setLoading(false);
161
- }
136
+ await executeSubscriptionOperation(
137
+ () => serviceCheck.service!.deactivateSubscription(userId),
138
+ setLoading,
139
+ setError,
140
+ (result) => setStatus(result)
141
+ );
162
142
  }, []);
163
143
 
164
144
  const isPremium = isSubscriptionValid(status);
@@ -0,0 +1,78 @@
1
+ /**
2
+ * useSubscription Utilities
3
+ * Shared utilities for subscription hook operations
4
+ */
5
+
6
+ export type AsyncSubscriptionOperation<T> = () => Promise<T>;
7
+
8
+ /**
9
+ * Result of a subscription service initialization check
10
+ */
11
+ export interface ServiceCheckResult {
12
+ success: boolean;
13
+ service: ReturnType<typeof import("../../infrastructure/services/SubscriptionService").getSubscriptionService> | null;
14
+ error?: string;
15
+ }
16
+
17
+ /**
18
+ * Checks if subscription service is initialized
19
+ * Returns service instance or error
20
+ */
21
+ export function checkSubscriptionService(): ServiceCheckResult {
22
+ const { getSubscriptionService } = require("../../infrastructure/services/SubscriptionService");
23
+ const service = getSubscriptionService();
24
+
25
+ if (!service) {
26
+ return {
27
+ success: false,
28
+ service: null,
29
+ error: "Subscription service is not initialized",
30
+ };
31
+ }
32
+
33
+ return { success: true, service, error: undefined };
34
+ }
35
+
36
+ /**
37
+ * Validates user ID
38
+ */
39
+ export function validateUserId(userId: string): string | null {
40
+ if (!userId) {
41
+ return "User ID is required";
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * Wraps async subscription operations with loading state, error handling, and state updates
48
+ */
49
+ export async function executeSubscriptionOperation<T>(
50
+ operation: AsyncSubscriptionOperation<T>,
51
+ setLoading: (loading: boolean) => void,
52
+ setError: (error: string | null) => void,
53
+ onSuccess?: (result: T) => void
54
+ ): Promise<void> {
55
+ setLoading(true);
56
+ setError(null);
57
+
58
+ try {
59
+ const result = await operation();
60
+ if (onSuccess) {
61
+ onSuccess(result);
62
+ }
63
+ } catch (err) {
64
+ const errorMessage =
65
+ err instanceof Error ? err.message : "Operation failed";
66
+ setError(errorMessage);
67
+ throw err;
68
+ } finally {
69
+ setLoading(false);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Formats error message from unknown error
75
+ */
76
+ export function formatErrorMessage(err: unknown, fallbackMessage: string): string {
77
+ return err instanceof Error ? err.message : fallbackMessage;
78
+ }
@@ -15,6 +15,7 @@ import {
15
15
  formatDateForLocale,
16
16
  calculateDaysRemaining,
17
17
  } from "../utils/subscriptionDateUtils";
18
+ import { useCreditsArray, getSubscriptionStatusType } from "./useSubscriptionSettingsConfig.utils";
18
19
  import type {
19
20
  SubscriptionSettingsConfig,
20
21
  SubscriptionStatusType,
@@ -104,23 +105,10 @@ export const useSubscriptionSettingsConfig = (
104
105
  );
105
106
 
106
107
  // Status type
107
- const statusType: SubscriptionStatusType = isPremium ? "active" : "none";
108
+ const statusType: SubscriptionStatusType = getSubscriptionStatusType(isPremium);
108
109
 
109
110
  // Credits array
110
- const creditsArray = useMemo(() => {
111
- if (!credits) return [];
112
- const total = getCreditLimit
113
- ? getCreditLimit(credits.imageCredits)
114
- : credits.imageCredits;
115
- return [
116
- {
117
- id: "image",
118
- label: translations.imageCreditsLabel || "Image Credits",
119
- current: credits.imageCredits,
120
- total,
121
- },
122
- ];
123
- }, [credits, getCreditLimit, translations.imageCreditsLabel]);
111
+ const creditsArray = useCreditsArray(credits, getCreditLimit, translations);
124
112
 
125
113
  // Build config
126
114
  const config = useMemo(
@@ -0,0 +1,48 @@
1
+ /**
2
+ * useSubscriptionSettingsConfig Utilities
3
+ * Helper functions for subscription settings config
4
+ */
5
+
6
+ import { useMemo } from "react";
7
+ import type { UserCredits } from "../../domain/entities/Credits";
8
+ import type { SubscriptionSettingsTranslations } from "../types/SubscriptionSettingsTypes";
9
+
10
+ export interface CreditsInfo {
11
+ id: string;
12
+ label: string;
13
+ current: number;
14
+ total: number;
15
+ }
16
+
17
+ /**
18
+ * Builds credits array for display
19
+ */
20
+ export function useCreditsArray(
21
+ credits: UserCredits | null | undefined,
22
+ getCreditLimit: ((credits: number) => number) | undefined,
23
+ translations: SubscriptionSettingsTranslations
24
+ ): CreditsInfo[] {
25
+ return useMemo(() => {
26
+ if (!credits) return [];
27
+ const total = getCreditLimit
28
+ ? getCreditLimit(credits.imageCredits)
29
+ : credits.imageCredits;
30
+ return [
31
+ {
32
+ id: "image",
33
+ label: translations.imageCreditsLabel || "Image Credits",
34
+ current: credits.imageCredits,
35
+ total,
36
+ },
37
+ ];
38
+ }, [credits, getCreditLimit, translations.imageCreditsLabel]);
39
+ }
40
+
41
+ /**
42
+ * Calculates subscription status type
43
+ */
44
+ export function getSubscriptionStatusType(
45
+ isPremium: boolean
46
+ ): "active" | "none" {
47
+ return isPremium ? "active" : "none";
48
+ }
@@ -0,0 +1,12 @@
1
+ export * from "./domain/errors/RevenueCatError";
2
+ export * from "./domain/value-objects/RevenueCatConfig";
3
+ export * from "./domain/types/RevenueCatTypes";
4
+ export * from "./domain/constants/RevenueCatConstants";
5
+ export * from "./application/ports/IRevenueCatService";
6
+ export * from "./infrastructure/services/RevenueCatService";
7
+ export * from "./infrastructure/managers/SubscriptionManager";
8
+ export * from "./infrastructure/handlers/PackageHandler";
9
+ export * from "./presentation/hooks/useRevenueCat";
10
+ export * from "./presentation/hooks/useCustomerInfo";
11
+ export * from "./presentation/hooks/usePaywallFlow";
12
+ export * from "./presentation/hooks/useSubscriptionQueries";
@@ -0,0 +1,15 @@
1
+ export * from "./aiCreditHelpers";
2
+ export * from "./authUtils";
3
+ export * from "./creditChecker";
4
+ export * from "./creditMapper";
5
+ export * from "./dateValidationUtils";
6
+ export * from "./packageFilter";
7
+ export * from "./packagePeriodUtils";
8
+ export * from "./packageTypeDetector";
9
+ export * from "./premiumAsyncUtils";
10
+ export * from "./premiumStatusUtils";
11
+ export * from "./priceUtils";
12
+ export * from "./tierUtils";
13
+ export * from "./types";
14
+ export * from "./userTierUtils";
15
+ export * from "./validation";