@umituz/react-native-subscription 2.11.20 → 2.11.22

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.11.20",
3
+ "version": "2.11.22",
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",
package/src/index.ts CHANGED
@@ -227,6 +227,7 @@ export {
227
227
  type UseDeductCreditResult,
228
228
  type UseInitializeCreditsParams,
229
229
  type UseInitializeCreditsResult,
230
+ type InitializeCreditsOptions,
230
231
  } from "./presentation/hooks/useDeductCredit";
231
232
 
232
233
  export {
@@ -274,6 +275,19 @@ export {
274
275
  type AICreditHelpers,
275
276
  } from "./utils/aiCreditHelpers";
276
277
 
278
+ export {
279
+ detectPackageType,
280
+ type SubscriptionPackageType,
281
+ } from "./utils/packageTypeDetector";
282
+
283
+ export {
284
+ getCreditAllocation,
285
+ getImageCreditsForPackage,
286
+ getTextCreditsForPackage,
287
+ CREDIT_ALLOCATIONS,
288
+ type CreditAllocation,
289
+ } from "./utils/creditMapper";
290
+
277
291
  // =============================================================================
278
292
  // REVENUECAT - Errors
279
293
  // =============================================================================
@@ -23,6 +23,8 @@ import type {
23
23
  } from "../../domain/entities/Credits";
24
24
  import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
25
25
  import { initializeCreditsTransaction } from "../services/CreditsInitializer";
26
+ import { detectPackageType } from "../../utils/packageTypeDetector";
27
+ import { getCreditAllocation } from "../../utils/creditMapper";
26
28
 
27
29
  export class CreditsRepository extends BaseRepository {
28
30
  private config: CreditsConfig;
@@ -79,7 +81,8 @@ export class CreditsRepository extends BaseRepository {
79
81
 
80
82
  async initializeCredits(
81
83
  userId: string,
82
- purchaseId?: string
84
+ purchaseId?: string,
85
+ productId?: string
83
86
  ): Promise<CreditsResult> {
84
87
  const db = getFirestore();
85
88
  if (!db) {
@@ -89,15 +92,63 @@ export class CreditsRepository extends BaseRepository {
89
92
  };
90
93
  }
91
94
 
95
+ if (__DEV__) {
96
+ console.log("[CreditsRepository] Initialize credits:", {
97
+ userId,
98
+ purchaseId,
99
+ productId,
100
+ });
101
+ }
102
+
92
103
  try {
93
104
  const creditsRef = this.getCreditsDocRef(db, userId);
105
+
106
+ // Determine credit allocation based on product ID
107
+ let configToUse = this.config;
108
+
109
+ if (productId) {
110
+ const packageType = detectPackageType(productId);
111
+ const allocation = getCreditAllocation(packageType);
112
+
113
+ if (allocation) {
114
+ // Override config with tier-specific credit amounts
115
+ configToUse = {
116
+ ...this.config,
117
+ imageCreditLimit: allocation.imageCredits,
118
+ textCreditLimit: allocation.textCredits,
119
+ };
120
+
121
+ if (__DEV__) {
122
+ console.log("[CreditsRepository] Using tier-based allocation:", {
123
+ packageType,
124
+ imageCredits: allocation.imageCredits,
125
+ textCredits: allocation.textCredits,
126
+ });
127
+ }
128
+ } else {
129
+ if (__DEV__) {
130
+ console.warn(
131
+ "[CreditsRepository] Could not determine package type, using default config:",
132
+ this.config
133
+ );
134
+ }
135
+ }
136
+ }
137
+
94
138
  const result = await initializeCreditsTransaction(
95
139
  db,
96
140
  creditsRef,
97
- this.config,
141
+ configToUse,
98
142
  purchaseId
99
143
  );
100
144
 
145
+ if (__DEV__) {
146
+ console.log("[CreditsRepository] Credits initialized successfully:", {
147
+ imageCredits: result.imageCredits,
148
+ textCredits: result.textCredits,
149
+ });
150
+ }
151
+
101
152
  return {
102
153
  success: true,
103
154
  data: {
@@ -108,6 +159,10 @@ export class CreditsRepository extends BaseRepository {
108
159
  },
109
160
  };
110
161
  } catch (error) {
162
+ if (__DEV__) {
163
+ console.error("[CreditsRepository] Failed to initialize credits:", error);
164
+ }
165
+
111
166
  return {
112
167
  success: false,
113
168
  error: {
@@ -21,6 +21,14 @@ export async function initializeCreditsTransaction(
21
21
  config: CreditsConfig,
22
22
  purchaseId?: string
23
23
  ): Promise<InitializationResult> {
24
+ if (__DEV__) {
25
+ console.log("[CreditsInitializer] Starting transaction with config:", {
26
+ textCreditLimit: config.textCreditLimit,
27
+ imageCreditLimit: config.imageCreditLimit,
28
+ purchaseId,
29
+ });
30
+ }
31
+
24
32
  return runTransaction(db, async (transaction: Transaction) => {
25
33
  const creditsDoc = await transaction.get(creditsRef);
26
34
  const now = serverTimestamp();
@@ -34,7 +42,21 @@ export async function initializeCreditsTransaction(
34
42
  const existing = creditsDoc.data() as UserCreditsDocumentRead;
35
43
  processedPurchases = existing.processedPurchases || [];
36
44
 
45
+ if (__DEV__) {
46
+ console.log("[CreditsInitializer] Existing credits found:", {
47
+ textCredits: existing.textCredits,
48
+ imageCredits: existing.imageCredits,
49
+ processedPurchases,
50
+ });
51
+ }
52
+
37
53
  if (purchaseId && processedPurchases.includes(purchaseId)) {
54
+ if (__DEV__) {
55
+ console.warn(
56
+ "[CreditsInitializer] Purchase already processed:",
57
+ purchaseId
58
+ );
59
+ }
38
60
  return {
39
61
  textCredits: existing.textCredits,
40
62
  imageCredits: existing.imageCredits,
@@ -44,9 +66,32 @@ export async function initializeCreditsTransaction(
44
66
 
45
67
  newTextCredits = (existing.textCredits || 0) + config.textCreditLimit;
46
68
  newImageCredits = (existing.imageCredits || 0) + config.imageCreditLimit;
69
+
70
+ if (__DEV__) {
71
+ console.log("[CreditsInitializer] Adding to existing credits:", {
72
+ existingText: existing.textCredits || 0,
73
+ existingImage: existing.imageCredits || 0,
74
+ adding: {
75
+ text: config.textCreditLimit,
76
+ image: config.imageCreditLimit,
77
+ },
78
+ newTotal: {
79
+ text: newTextCredits,
80
+ image: newImageCredits,
81
+ },
82
+ });
83
+ }
84
+
47
85
  if (existing.purchasedAt) {
48
86
  purchasedAt = existing.purchasedAt as unknown as FieldValue;
49
87
  }
88
+ } else {
89
+ if (__DEV__) {
90
+ console.log("[CreditsInitializer] Creating new credits document:", {
91
+ textCredits: newTextCredits,
92
+ imageCredits: newImageCredits,
93
+ });
94
+ }
50
95
  }
51
96
 
52
97
  if (purchaseId) {
@@ -62,6 +107,13 @@ export async function initializeCreditsTransaction(
62
107
  processedPurchases,
63
108
  });
64
109
 
110
+ if (__DEV__) {
111
+ console.log("[CreditsInitializer] Transaction completed successfully:", {
112
+ textCredits: newTextCredits,
113
+ imageCredits: newImageCredits,
114
+ });
115
+ }
116
+
65
117
  return { textCredits: newTextCredits, imageCredits: newImageCredits };
66
118
  });
67
119
  }
@@ -19,7 +19,6 @@ export const PlanCardHeader: React.FC<PlanCardHeaderProps> = ({
19
19
  price,
20
20
  creditAmount,
21
21
  isSelected,
22
- isExpanded,
23
22
  isBestValue,
24
23
  onToggle,
25
24
  }) => {
@@ -106,8 +106,13 @@ export interface UseInitializeCreditsParams {
106
106
  userId: string | undefined;
107
107
  }
108
108
 
109
+ export interface InitializeCreditsOptions {
110
+ purchaseId?: string;
111
+ productId?: string;
112
+ }
113
+
109
114
  export interface UseInitializeCreditsResult {
110
- initializeCredits: (purchaseId?: string) => Promise<boolean>;
115
+ initializeCredits: (options?: InitializeCreditsOptions) => Promise<boolean>;
111
116
  isInitializing: boolean;
112
117
  }
113
118
 
@@ -118,14 +123,31 @@ export const useInitializeCredits = ({
118
123
  const queryClient = useQueryClient();
119
124
 
120
125
  const mutation = useMutation({
121
- mutationFn: async (purchaseId?: string) => {
126
+ mutationFn: async (options?: InitializeCreditsOptions) => {
122
127
  if (!userId) {
123
128
  throw new Error("User not authenticated");
124
129
  }
125
- return repository.initializeCredits(userId, purchaseId);
130
+
131
+ if (__DEV__) {
132
+ console.log("[useInitializeCredits] Initializing credits:", {
133
+ userId,
134
+ purchaseId: options?.purchaseId,
135
+ productId: options?.productId,
136
+ });
137
+ }
138
+
139
+ return repository.initializeCredits(
140
+ userId,
141
+ options?.purchaseId,
142
+ options?.productId
143
+ );
126
144
  },
127
145
  onSuccess: (result) => {
128
146
  if (userId && result.success && result.data) {
147
+ if (__DEV__) {
148
+ console.log("[useInitializeCredits] Success, updating cache:", result.data);
149
+ }
150
+
129
151
  // Set the data immediately for optimistic UI
130
152
  queryClient.setQueryData(creditsQueryKeys.user(userId), result.data);
131
153
  // Also invalidate to ensure all subscribers get the update
@@ -134,16 +156,24 @@ export const useInitializeCredits = ({
134
156
  });
135
157
  }
136
158
  },
159
+ onError: (error) => {
160
+ if (__DEV__) {
161
+ console.error("[useInitializeCredits] Error:", error);
162
+ }
163
+ },
137
164
  });
138
165
 
139
- const initializeCredits = useCallback(async (purchaseId?: string): Promise<boolean> => {
140
- try {
141
- const result = await mutation.mutateAsync(purchaseId);
142
- return result.success;
143
- } catch {
144
- return false;
145
- }
146
- }, [mutation]);
166
+ const initializeCredits = useCallback(
167
+ async (options?: InitializeCreditsOptions): Promise<boolean> => {
168
+ try {
169
+ const result = await mutation.mutateAsync(options);
170
+ return result.success;
171
+ } catch {
172
+ return false;
173
+ }
174
+ },
175
+ [mutation]
176
+ );
147
177
 
148
178
  return {
149
179
  initializeCredits,
@@ -67,7 +67,7 @@ export const usePremiumWithConfig = (
67
67
  async (pkg: PurchasesPackage): Promise<boolean> => {
68
68
  const success = await purchaseMutation.mutateAsync(pkg);
69
69
  if (success && userId) {
70
- await initializeCredits();
70
+ await initializeCredits({ productId: pkg.product.identifier });
71
71
  }
72
72
  return success;
73
73
  },
@@ -14,24 +14,31 @@ import {
14
14
 
15
15
  /**
16
16
  * Fetch available subscription packages
17
+ * Works for both authenticated and anonymous users
17
18
  */
18
19
  export const useSubscriptionPackages = (userId: string | undefined) => {
19
20
  return useQuery({
20
- queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, userId] as const,
21
+ queryKey: [...SUBSCRIPTION_QUERY_KEYS.packages, userId ?? "anonymous"] as const,
21
22
  queryFn: async () => {
22
23
  addPackageBreadcrumb("subscription", "Fetch packages query started", {
23
- userId: userId ?? "NO_USER",
24
+ userId: userId ?? "ANONYMOUS",
24
25
  });
25
26
 
26
- // Skip if already initialized for this specific user
27
- if (!userId || !SubscriptionManager.isInitializedForUser(userId)) {
28
- await SubscriptionManager.initialize(userId);
27
+ // Initialize if needed (works for both authenticated and anonymous users)
28
+ if (userId) {
29
+ if (!SubscriptionManager.isInitializedForUser(userId)) {
30
+ await SubscriptionManager.initialize(userId);
31
+ }
32
+ } else {
33
+ if (!SubscriptionManager.isInitialized()) {
34
+ await SubscriptionManager.initialize(undefined);
35
+ }
29
36
  }
30
37
 
31
38
  const packages = await SubscriptionManager.getPackages();
32
39
 
33
40
  addPackageBreadcrumb("subscription", "Fetch packages query success", {
34
- userId: userId ?? "NO_USER",
41
+ userId: userId ?? "ANONYMOUS",
35
42
  count: packages.length,
36
43
  });
37
44
 
@@ -39,6 +46,6 @@ export const useSubscriptionPackages = (userId: string | undefined) => {
39
46
  },
40
47
  staleTime: STALE_TIME,
41
48
  gcTime: GC_TIME,
42
- enabled: !!userId, // Only run when userId is available
49
+ enabled: true, // Always enabled - works for both authenticated and anonymous users
43
50
  });
44
51
  };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Credit Mapper
3
+ * Maps subscription package types to credit amounts
4
+ * Based on SUBSCRIPTION_GUIDE.md pricing strategy
5
+ */
6
+
7
+ import type { SubscriptionPackageType } from "./packageTypeDetector";
8
+
9
+ export interface CreditAllocation {
10
+ imageCredits: number;
11
+ textCredits: number;
12
+ }
13
+
14
+ /**
15
+ * Standard credit allocations per package type
16
+ * Based on profitability analysis and value ladder strategy
17
+ *
18
+ * Weekly: 6 images - $2.99 (62% margin) - Trial users
19
+ * Monthly: 25 images - $9.99 (60% margin) - Regular users
20
+ * Yearly: 300 images - $79.99 (55% margin) - Best value (46% cheaper/image)
21
+ */
22
+ export const CREDIT_ALLOCATIONS: Record<
23
+ Exclude<SubscriptionPackageType, "unknown">,
24
+ CreditAllocation
25
+ > = {
26
+ weekly: {
27
+ imageCredits: 6,
28
+ textCredits: 6,
29
+ },
30
+ monthly: {
31
+ imageCredits: 25,
32
+ textCredits: 25,
33
+ },
34
+ yearly: {
35
+ imageCredits: 300,
36
+ textCredits: 300,
37
+ },
38
+ };
39
+
40
+ /**
41
+ * Get credit allocation for a package type
42
+ * Returns null for unknown package types to prevent incorrect credit assignment
43
+ */
44
+ export function getCreditAllocation(
45
+ packageType: SubscriptionPackageType
46
+ ): CreditAllocation | null {
47
+ if (packageType === "unknown") {
48
+ if (__DEV__) {
49
+ console.warn(
50
+ "[CreditMapper] Cannot allocate credits for unknown package type"
51
+ );
52
+ }
53
+ return null;
54
+ }
55
+
56
+ const allocation = CREDIT_ALLOCATIONS[packageType];
57
+
58
+ if (__DEV__) {
59
+ console.log("[CreditMapper] Credit allocation for", packageType, ":", allocation);
60
+ }
61
+
62
+ return allocation;
63
+ }
64
+
65
+ /**
66
+ * Get image credits for a package type
67
+ */
68
+ export function getImageCreditsForPackage(
69
+ packageType: SubscriptionPackageType
70
+ ): number | null {
71
+ const allocation = getCreditAllocation(packageType);
72
+ return allocation?.imageCredits ?? null;
73
+ }
74
+
75
+ /**
76
+ * Get text credits for a package type
77
+ */
78
+ export function getTextCreditsForPackage(
79
+ packageType: SubscriptionPackageType
80
+ ): number | null {
81
+ const allocation = getCreditAllocation(packageType);
82
+ return allocation?.textCredits ?? null;
83
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Package Type Detector
3
+ * Detects subscription package type from RevenueCat package identifier
4
+ */
5
+
6
+ export type SubscriptionPackageType = "weekly" | "monthly" | "yearly" | "unknown";
7
+
8
+ /**
9
+ * Detect package type from product identifier
10
+ * Supports common RevenueCat naming patterns:
11
+ * - premium_weekly, weekly_premium, premium-weekly
12
+ * - premium_monthly, monthly_premium, premium-monthly
13
+ * - premium_yearly, yearly_premium, premium-yearly, premium_annual, annual_premium
14
+ */
15
+ export function detectPackageType(productIdentifier: string): SubscriptionPackageType {
16
+ if (!productIdentifier) {
17
+ if (__DEV__) {
18
+ console.log("[PackageTypeDetector] No product identifier provided");
19
+ }
20
+ return "unknown";
21
+ }
22
+
23
+ const normalized = productIdentifier.toLowerCase();
24
+
25
+ if (__DEV__) {
26
+ console.log("[PackageTypeDetector] Detecting package type for:", normalized);
27
+ }
28
+
29
+ // Weekly detection
30
+ if (normalized.includes("weekly") || normalized.includes("week")) {
31
+ if (__DEV__) {
32
+ console.log("[PackageTypeDetector] Detected: WEEKLY");
33
+ }
34
+ return "weekly";
35
+ }
36
+
37
+ // Monthly detection
38
+ if (normalized.includes("monthly") || normalized.includes("month")) {
39
+ if (__DEV__) {
40
+ console.log("[PackageTypeDetector] Detected: MONTHLY");
41
+ }
42
+ return "monthly";
43
+ }
44
+
45
+ // Yearly detection (includes annual)
46
+ if (
47
+ normalized.includes("yearly") ||
48
+ normalized.includes("year") ||
49
+ normalized.includes("annual")
50
+ ) {
51
+ if (__DEV__) {
52
+ console.log("[PackageTypeDetector] Detected: YEARLY");
53
+ }
54
+ return "yearly";
55
+ }
56
+
57
+ if (__DEV__) {
58
+ console.warn("[PackageTypeDetector] Unknown package type for:", productIdentifier);
59
+ }
60
+
61
+ return "unknown";
62
+ }