@umituz/react-native-subscription 2.26.6 → 2.26.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.26.6",
3
+ "version": "2.26.8",
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",
@@ -74,6 +74,10 @@ export interface CreditsConfig {
74
74
  creditPackageAmounts?: Record<string, number>;
75
75
  /** Credit allocations for different subscription types (weekly, monthly, yearly) */
76
76
  packageAllocations?: PackageAllocationMap;
77
+ /** Free credits given to new users on registration (default: 0) */
78
+ freeCredits?: number;
79
+ /** Whether to auto-initialize free credits when user has no credits document (default: true if freeCredits > 0) */
80
+ autoInitializeFreeCredits?: boolean;
77
81
  }
78
82
 
79
83
  export interface CreditsResult<T = UserCredits> {
@@ -7,6 +7,7 @@ export const SUBSCRIPTION_STATUS = {
7
7
  EXPIRED: 'expired',
8
8
  CANCELED: 'canceled',
9
9
  NONE: 'none',
10
+ FREE: 'free',
10
11
  } as const;
11
12
 
12
13
  /** RevenueCat period type constants */
@@ -133,6 +133,72 @@ export class CreditsRepository extends BaseRepository {
133
133
  return !!(res.success && res.data && res.data.credits >= cost);
134
134
  }
135
135
 
136
+ /**
137
+ * Initialize free credits for new users
138
+ * Creates a credits document with freeCredits amount (no subscription)
139
+ */
140
+ async initializeFreeCredits(userId: string): Promise<CreditsResult> {
141
+ const db = getFirestore();
142
+ if (!db) return { success: false, error: { message: "No DB", code: "INIT_ERR" } };
143
+
144
+ const freeCredits = this.config.freeCredits ?? 0;
145
+ if (freeCredits <= 0) {
146
+ return { success: false, error: { message: "Free credits not configured", code: "NO_FREE_CREDITS" } };
147
+ }
148
+
149
+ try {
150
+ const ref = this.getRef(db, userId);
151
+ const snap = await getDoc(ref);
152
+
153
+ // Don't overwrite if document already exists
154
+ if (snap.exists()) {
155
+ if (__DEV__) console.log("[CreditsRepository] Credits document already exists, skipping free credits init");
156
+ const existing = snap.data() as UserCreditsDocumentRead;
157
+ return { success: true, data: CreditsMapper.toEntity(existing) };
158
+ }
159
+
160
+ // Create new document with free credits
161
+ const { setDoc } = await import("firebase/firestore");
162
+ const now = serverTimestamp();
163
+
164
+ const creditsData = {
165
+ // Not premium - just free credits
166
+ isPremium: false,
167
+ status: "free" as const,
168
+
169
+ // Free credits
170
+ credits: freeCredits,
171
+ creditLimit: freeCredits,
172
+ isFreeCredits: true,
173
+
174
+ // Dates
175
+ createdAt: now,
176
+ lastUpdatedAt: now,
177
+ };
178
+
179
+ await setDoc(ref, creditsData);
180
+
181
+ if (__DEV__) console.log("[CreditsRepository] Initialized free credits:", { userId: userId.slice(0, 8), credits: freeCredits });
182
+
183
+ return {
184
+ success: true,
185
+ data: {
186
+ isPremium: false,
187
+ status: "free",
188
+ credits: freeCredits,
189
+ creditLimit: freeCredits,
190
+ purchasedAt: null,
191
+ expirationDate: null,
192
+ lastUpdatedAt: null,
193
+ willRenew: false,
194
+ }
195
+ };
196
+ } catch (e: any) {
197
+ if (__DEV__) console.error("[CreditsRepository] Free credits init error:", e.message);
198
+ return { success: false, error: { message: e.message, code: "INIT_ERR" } };
199
+ }
200
+ }
201
+
136
202
  /** Sync expired subscription status to Firestore (background) */
137
203
  async syncExpiredStatus(userId: string): Promise<void> {
138
204
  const db = getFirestore();
@@ -3,10 +3,11 @@
3
3
  *
4
4
  * TanStack Query hook for fetching user credits.
5
5
  * Generic and reusable - uses config from module-level provider.
6
+ * Auto-initializes free credits for new users if configured.
6
7
  */
7
8
 
8
9
  import { useQuery } from "@umituz/react-native-design-system";
9
- import { useCallback, useMemo } from "react";
10
+ import { useCallback, useMemo, useRef, useEffect } from "react";
10
11
  import type { UserCredits } from "../../domain/entities/Credits";
11
12
  import {
12
13
  getCreditsRepository,
@@ -65,7 +66,10 @@ export const useCredits = ({
65
66
 
66
67
  const queryEnabled = enabled && !!userId && isConfigured;
67
68
 
68
- const { data, isLoading, error, refetch } = useQuery({
69
+ // Track if free credits initialization has been attempted
70
+ const freeCreditsInitAttemptedRef = useRef<string | null>(null);
71
+
72
+ const { data, isLoading, error, refetch, isFetched } = useQuery({
69
73
  queryKey: creditsQueryKeys.user(userId ?? ""),
70
74
  queryFn: async () => {
71
75
  if (!userId || !isConfigured) {
@@ -98,6 +102,49 @@ export const useCredits = ({
98
102
 
99
103
  const credits = data ?? null;
100
104
 
105
+ // Auto-initialize free credits for new users
106
+ const freeCredits = config.freeCredits ?? 0;
107
+ const autoInit = config.autoInitializeFreeCredits !== false && freeCredits > 0;
108
+
109
+ useEffect(() => {
110
+ // Only run if:
111
+ // 1. Query has completed (isFetched)
112
+ // 2. User is authenticated
113
+ // 3. No credits data exists
114
+ // 4. Free credits configured
115
+ // 5. Auto-init enabled
116
+ // 6. Haven't already attempted for this user
117
+ if (
118
+ isFetched &&
119
+ userId &&
120
+ isConfigured &&
121
+ !credits &&
122
+ autoInit &&
123
+ freeCreditsInitAttemptedRef.current !== userId
124
+ ) {
125
+ freeCreditsInitAttemptedRef.current = userId;
126
+
127
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
128
+ console.log("[useCredits] Auto-initializing free credits for new user:", userId.slice(0, 8));
129
+ }
130
+
131
+ const repository = getCreditsRepository();
132
+ repository.initializeFreeCredits(userId).then((result) => {
133
+ if (result.success) {
134
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
135
+ console.log("[useCredits] Free credits initialized:", result.data?.credits);
136
+ }
137
+ // Refetch to get the new credits
138
+ refetch();
139
+ } else {
140
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
141
+ console.warn("[useCredits] Free credits init failed:", result.error?.message);
142
+ }
143
+ }
144
+ });
145
+ }
146
+ }, [isFetched, userId, isConfigured, credits, autoInit, refetch]);
147
+
101
148
  // Memoize derived values to prevent unnecessary re-renders
102
149
  const derivedValues = useMemo(() => {
103
150
  const has = (credits?.credits ?? 0) > 0;
@@ -11,6 +11,8 @@ import { creditsQueryKeys } from "./useCredits";
11
11
 
12
12
  import { timezoneService } from "@umituz/react-native-design-system";
13
13
 
14
+ declare const __DEV__: boolean;
15
+
14
16
  export interface UseDeductCreditParams {
15
17
  userId: string | undefined;
16
18
  onCreditsExhausted?: () => void;
@@ -61,14 +63,28 @@ export const useDeductCredit = ({
61
63
  });
62
64
 
63
65
  const deductCredit = useCallback(async (cost: number = 1): Promise<boolean> => {
66
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
67
+ console.log("[useDeductCredit] Attempting to deduct:", cost);
68
+ }
64
69
  try {
65
70
  const res = await mutation.mutateAsync(cost);
66
71
  if (!res.success) {
72
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
73
+ console.log("[useDeductCredit] Deduction failed:", res.error?.code, res.error?.message);
74
+ }
67
75
  if (res.error?.code === "CREDITS_EXHAUSTED") onCreditsExhausted?.();
68
76
  return false;
69
77
  }
78
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
79
+ console.log("[useDeductCredit] Deduction successful, remaining:", res.remainingCredits);
80
+ }
70
81
  return true;
71
- } catch { return false; }
82
+ } catch (err) {
83
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
84
+ console.log("[useDeductCredit] Deduction error:", err);
85
+ }
86
+ return false;
87
+ }
72
88
  }, [mutation, onCreditsExhausted]);
73
89
 
74
90
  const deductCredits = useCallback(async (cost: number): Promise<boolean> => {