@umituz/react-native-subscription 2.27.89 → 2.27.91

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.27.89",
3
+ "version": "2.27.91",
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",
@@ -66,24 +66,6 @@ export const useCredits = (): UseCreditsResult => {
66
66
  throw new Error(result.error?.message || "Failed to fetch credits");
67
67
  }
68
68
 
69
- // If subscription is expired, immediately return 0 credits
70
- // to prevent any window where expired user could deduct
71
- if (result.data?.status === "expired") {
72
- // Sync to Firestore in background
73
- repository.syncExpiredStatus(userId).catch((syncError) => {
74
- if (typeof __DEV__ !== "undefined" && __DEV__) {
75
- console.warn("[useCredits] Background sync failed:", syncError);
76
- }
77
- });
78
-
79
- // Return expired data with 0 credits immediately
80
- return {
81
- ...result.data,
82
- credits: 0,
83
- isPremium: false,
84
- };
85
- }
86
-
87
69
  return result.data || null;
88
70
  },
89
71
  enabled: queryEnabled,
@@ -8,6 +8,7 @@ import { useMutation, useQueryClient } from "@umituz/react-native-design-system"
8
8
  import type { UserCredits } from "../core/Credits";
9
9
  import { getCreditsRepository } from "../infrastructure/CreditsRepositoryProvider";
10
10
  import { creditsQueryKeys } from "./useCredits";
11
+ import { calculateRemainingCredits } from "../utils/creditCalculations";
11
12
 
12
13
  import { timezoneService } from "@umituz/react-native-design-system";
13
14
 
@@ -42,14 +43,12 @@ export const useDeductCredit = ({
42
43
  await queryClient.cancelQueries({ queryKey: creditsQueryKeys.user(userId) });
43
44
  const previousCredits = queryClient.getQueryData<UserCredits>(creditsQueryKeys.user(userId));
44
45
 
45
- // Improved optimistic update logic
46
46
  if (!previousCredits) {
47
47
  return { previousCredits: null, skippedOptimistic: true };
48
48
  }
49
49
 
50
- // If credits are insufficient, show 0 but don't skip optimistic update
51
- // This provides better UX by showing the user what will happen
52
- const newCredits = Math.max(0, previousCredits.credits - cost);
50
+ // Calculate new credits using utility
51
+ const newCredits = calculateRemainingCredits(previousCredits.credits, cost);
53
52
 
54
53
  queryClient.setQueryData<UserCredits | null>(creditsQueryKeys.user(userId), (old) => {
55
54
  if (!old) return old;
@@ -67,10 +66,10 @@ export const useDeductCredit = ({
67
66
  };
68
67
  },
69
68
  onError: (_err, _cost, context) => {
70
- // Restore previous credits on error
71
- // Skip restoration if credits were insufficient (optimistic update showed 0, which is correct)
72
- if (userId && context?.previousCredits && !context.skippedOptimistic && !context.wasInsufficient) {
73
- queryClient.setQueryData(creditsQueryKeys.user(userId), context.previousCredits);
69
+ // Always restore previous credits on error to prevent UI desync
70
+ // Use optional chaining to be safe
71
+ if (userId && context?.previousCredits && !context.skippedOptimistic) {
72
+ queryClient.setQueryData(creditsQueryKeys.user(userId), context.previousCredits);
74
73
  }
75
74
  },
76
75
  onSuccess: () => {
@@ -24,3 +24,10 @@ export const canAffordCost = (
24
24
  }
25
25
  return currentCredits >= cost;
26
26
  };
27
+
28
+ export const calculateRemainingCredits = (
29
+ currentCredits: number,
30
+ cost: number
31
+ ): number => {
32
+ return Math.max(0, currentCredits - cost);
33
+ };
@@ -103,7 +103,7 @@ All hooks follow consistent patterns:
103
103
  - **ALWAYS** handle loading and error states
104
104
  - **NEVER** trust client-side state for security
105
105
  - **MUST** implement error boundaries
106
- - **ALWAYS** test with various user states (guest, free, premium)
106
+ - **ALWAYS** test with various user states (anonymous, free, premium)
107
107
 
108
108
  ## AI Agent Guidelines
109
109
 
@@ -40,7 +40,7 @@ Hook for checking and managing premium subscription status.
40
40
 
41
41
  - **NEVER** use for security decisions (server-side validation required)
42
42
  - **NEVER** assume instant data availability (always check loading state)
43
- - **DO NOT** use for guest users without proper handling
43
+ - **DO NOT** use for anonymous users without proper handling
44
44
 
45
45
  ### CRITICAL SAFETY
46
46
 
@@ -67,7 +67,7 @@ Hook for checking and managing premium subscription status.
67
67
  - [ ] Provide upgrade path for free users
68
68
  - [ ] Test with premium user
69
69
  - [ ] Test with free user
70
- - [ ] Test with guest user
70
+ - [ ] Test with anonymous user
71
71
  - [ ] Test offline scenario
72
72
 
73
73
  ### Common Patterns
@@ -1,6 +1,4 @@
1
- export * from "./aiCreditHelpers";
2
- export * from "./authUtils";
3
- export * from "./creditChecker";
1
+
4
2
  export * from "./creditMapper";
5
3
  export * from "./packagePeriodUtils";
6
4
  export * from "./packageTypeDetector";
@@ -16,8 +16,10 @@ export type SubscriptionPackageType = PackageType;
16
16
  * Credit packages use a different system and don't need type detection
17
17
  */
18
18
  export function isCreditPackage(identifier: string): boolean {
19
+ if (!identifier) return false;
19
20
  // Matches "credit" as a word or part of a common naming pattern
20
- return /\bcredit\b|_credit_|-credit-/i.test(identifier) || identifier.toLowerCase().includes("credit");
21
+ // More strict to avoid false positives (e.g. "accredited")
22
+ return /(?:^|[._-])credit(?:$|[._-])/i.test(identifier);
21
23
  }
22
24
 
23
25
  /**
@@ -36,37 +38,25 @@ export function detectPackageType(productIdentifier: string): SubscriptionPackag
36
38
  return PACKAGE_TYPE.UNKNOWN;
37
39
  }
38
40
 
39
- // Preview API mode (Expo Go testing)
40
- if (normalized.includes("preview")) {
41
- if (__DEV__) {
42
- console.log("[PackageTypeDetector] Detected: PREVIEW (monthly)");
43
- }
44
- return PACKAGE_TYPE.MONTHLY;
45
- }
46
-
47
41
  // Weekly detection: matches "weekly" or "week" as distinct parts of the ID
48
42
  if (/\bweekly?\b|_week_|-week-|\.week\./i.test(normalized)) {
49
- if (__DEV__) {
50
- console.log("[PackageTypeDetector] Detected: WEEKLY");
51
- }
52
43
  return PACKAGE_TYPE.WEEKLY;
53
44
  }
54
45
 
55
46
  // Monthly detection: matches "monthly" or "month"
56
47
  if (/\bmonthly?\b|_month_|-month-|\.month\./i.test(normalized)) {
57
- if (__DEV__) {
58
- console.log("[PackageTypeDetector] Detected: MONTHLY");
59
- }
60
48
  return PACKAGE_TYPE.MONTHLY;
61
49
  }
62
50
 
63
51
  // Yearly detection: matches "yearly", "year", or "annual"
64
52
  if (/\byearly?\b|_year_|-year-|\.year\.|annual/i.test(normalized)) {
65
- if (__DEV__) {
66
- console.log("[PackageTypeDetector] Detected: YEARLY");
67
- }
68
53
  return PACKAGE_TYPE.YEARLY;
69
54
  }
55
+
56
+ // Lifetime detection: matches "lifetime"
57
+ if (/\blifetime\b|_lifetime_|-lifetime-|\.lifetime\./i.test(normalized)) {
58
+ return PACKAGE_TYPE.LIFETIME;
59
+ }
70
60
 
71
61
  if (__DEV__) {
72
62
  console.warn("[PackageTypeDetector] Unknown package type for:", productIdentifier);
@@ -5,18 +5,18 @@
5
5
  */
6
6
 
7
7
  import type { PremiumStatusFetcher } from './types';
8
- import { isGuest } from './authUtils';
8
+
9
9
 
10
10
  /**
11
11
  * Get isPremium value with centralized logic
12
12
  */
13
13
  export function getIsPremium(
14
- isGuestFlag: boolean,
14
+ isAnonymous: boolean,
15
15
  userId: string | null,
16
16
  isPremiumOrFetcher: boolean | PremiumStatusFetcher,
17
17
  ): Promise<boolean> {
18
- // Guest users NEVER have premium
19
- if (isGuest(isGuestFlag, userId)) return Promise.resolve(false);
18
+ // Anonymous users NEVER have premium
19
+ if (isAnonymous || userId === null) return Promise.resolve(false);
20
20
 
21
21
  // Sync mode: return the provided isPremium value
22
22
  if (typeof isPremiumOrFetcher === 'boolean') return Promise.resolve(isPremiumOrFetcher);
@@ -5,18 +5,18 @@
5
5
  */
6
6
 
7
7
  import type { UserTierInfo } from './types';
8
- import { isGuest } from './authUtils';
8
+
9
9
 
10
10
  export function getUserTierInfo(
11
- isGuestFlag: boolean,
11
+ isAnonymous: boolean,
12
12
  userId: string | null,
13
13
  isPremium: boolean,
14
14
  ): UserTierInfo {
15
- if (isGuest(isGuestFlag, userId)) {
15
+ if (isAnonymous || userId === null) {
16
16
  return {
17
- tier: 'guest',
17
+ tier: 'anonymous',
18
18
  isPremium: false,
19
- isGuest: true,
19
+ isAnonymous: true,
20
20
  isAuthenticated: false,
21
21
  userId: null,
22
22
  };
@@ -25,17 +25,17 @@ export function getUserTierInfo(
25
25
  return {
26
26
  tier: isPremium ? 'premium' : 'freemium',
27
27
  isPremium,
28
- isGuest: false,
28
+ isAnonymous: false,
29
29
  isAuthenticated: true,
30
30
  userId,
31
31
  };
32
32
  }
33
33
 
34
34
  export function checkPremiumAccess(
35
- isGuestFlag: boolean,
35
+ isAnonymous: boolean,
36
36
  userId: string | null,
37
37
  isPremium: boolean,
38
38
  ): boolean {
39
- if (isGuest(isGuestFlag, userId)) return false;
39
+ if (isAnonymous || userId === null) return false;
40
40
  return isPremium;
41
41
  }
@@ -4,7 +4,7 @@
4
4
  * Type definitions for user tier system
5
5
  */
6
6
 
7
- export type UserTier = 'guest' | 'freemium' | 'premium';
7
+ export type UserTier = 'anonymous' | 'freemium' | 'premium';
8
8
 
9
9
  export interface UserTierInfo {
10
10
  /** User tier classification */
@@ -13,13 +13,13 @@ export interface UserTierInfo {
13
13
  /** Whether user has premium access */
14
14
  isPremium: boolean;
15
15
 
16
- /** Whether user is a guest (not authenticated) */
17
- isGuest: boolean;
16
+ /** Whether user is anonymous (not authenticated) */
17
+ isAnonymous: boolean;
18
18
 
19
19
  /** Whether user is authenticated */
20
20
  isAuthenticated: boolean;
21
21
 
22
- /** User ID (null for guests) */
22
+ /** User ID (null for anonymous users) */
23
23
  userId: string | null;
24
24
  }
25
25
 
@@ -7,7 +7,7 @@
7
7
  import type { UserTier, UserTierInfo } from './types';
8
8
 
9
9
  export function isValidUserTier(value: unknown): value is UserTier {
10
- return value === 'guest' || value === 'freemium' || value === 'premium';
10
+ return value === 'anonymous' || value === 'freemium' || value === 'premium';
11
11
  }
12
12
 
13
13
  export function isUserTierInfo(value: unknown): value is UserTierInfo {
@@ -16,7 +16,7 @@ export function isUserTierInfo(value: unknown): value is UserTierInfo {
16
16
  return (
17
17
  isValidUserTier(obj.tier) &&
18
18
  typeof obj.isPremium === 'boolean' &&
19
- typeof obj.isGuest === 'boolean' &&
19
+ typeof obj.isAnonymous === 'boolean' &&
20
20
  typeof obj.isAuthenticated === 'boolean' &&
21
21
  (obj.userId === null || typeof obj.userId === 'string')
22
22
  );
@@ -1,37 +0,0 @@
1
- /**
2
- * Authentication Utilities Tests
3
- *
4
- * Tests for authentication check functions
5
- */
6
-
7
- import { isAuthenticated, isGuest } from '../authUtils';
8
-
9
- describe('isAuthenticated', () => {
10
- it('should return false for guest users', () => {
11
- expect(isAuthenticated(true, null)).toBe(false);
12
- expect(isAuthenticated(true, 'user123')).toBe(false);
13
- });
14
-
15
- it('should return false when userId is null', () => {
16
- expect(isAuthenticated(false, null)).toBe(false);
17
- });
18
-
19
- it('should return true for authenticated users', () => {
20
- expect(isAuthenticated(false, 'user123')).toBe(true);
21
- });
22
- });
23
-
24
- describe('isGuest', () => {
25
- it('should return true for guest users', () => {
26
- expect(isGuest(true, null)).toBe(true);
27
- expect(isGuest(true, 'user123')).toBe(true);
28
- });
29
-
30
- it('should return true when userId is null', () => {
31
- expect(isGuest(false, null)).toBe(true);
32
- });
33
-
34
- it('should return false for authenticated users', () => {
35
- expect(isGuest(false, 'user123')).toBe(false);
36
- });
37
- });
@@ -1,79 +0,0 @@
1
- /**
2
- * Edge Cases Tests
3
- *
4
- * Tests for edge cases and special scenarios
5
- */
6
-
7
- import { getUserTierInfo } from '../tierUtils';
8
- import { isAuthenticated, isGuest } from '../authUtils';
9
- import { validateUserId } from '../validation';
10
-
11
- describe('Edge Cases', () => {
12
- describe('User ID validation', () => {
13
- it('should handle empty string userId as invalid', () => {
14
- expect(() => validateUserId('')).toThrow(TypeError);
15
- });
16
-
17
- it('should handle whitespace-only userId as invalid', () => {
18
- expect(() => validateUserId(' ')).toThrow(TypeError);
19
- });
20
-
21
- it('should handle very long userId strings', () => {
22
- const longUserId = 'a'.repeat(1000);
23
- const result = getUserTierInfo(false, longUserId, true);
24
- expect(result.userId).toBe(longUserId);
25
- expect(result.tier).toBe('premium');
26
- });
27
-
28
- it('should handle special characters in userId', () => {
29
- const specialUserId = 'user-123_test@example.com';
30
- const result = getUserTierInfo(false, specialUserId, false);
31
- expect(result.userId).toBe(specialUserId);
32
- expect(result.tier).toBe('freemium');
33
- });
34
- });
35
-
36
- describe('Authentication edge cases', () => {
37
- it('should handle conflicting auth states consistently', () => {
38
- // isGuest=true but userId provided - should prioritize guest logic
39
- expect(isAuthenticated(true, 'user123')).toBe(false);
40
- expect(isGuest(true, 'user123')).toBe(true);
41
-
42
- const result = getUserTierInfo(true, 'user123', true);
43
- expect(result.tier).toBe('guest');
44
- expect(result.isPremium).toBe(false);
45
- });
46
-
47
- it('should handle isGuest=false but null userId', () => {
48
- // isGuest=false but userId=null - should treat as guest
49
- expect(isAuthenticated(false, null)).toBe(false);
50
- expect(isGuest(false, null)).toBe(true);
51
-
52
- const result = getUserTierInfo(false, null, true);
53
- expect(result.tier).toBe('guest');
54
- expect(result.isPremium).toBe(false);
55
- });
56
- });
57
-
58
- describe('Premium status edge cases', () => {
59
- it('should ignore isPremium for guest users regardless of value', () => {
60
- const guestTrue = getUserTierInfo(true, null, true);
61
- const guestFalse = getUserTierInfo(true, null, false);
62
-
63
- expect(guestTrue.isPremium).toBe(false);
64
- expect(guestFalse.isPremium).toBe(false);
65
- expect(guestTrue.tier).toBe('guest');
66
- expect(guestFalse.tier).toBe('guest');
67
- });
68
-
69
- it('should handle authenticated users with various premium states', () => {
70
- const premium = getUserTierInfo(false, 'user123', true);
71
- const freemium = getUserTierInfo(false, 'user123', false);
72
-
73
- expect(premium.tier).toBe('premium');
74
- expect(premium.isPremium).toBe(true);
75
- expect(freemium.tier).toBe('freemium');
76
- expect(freemium.isPremium).toBe(false);
77
- });
78
- });
79
- });
@@ -1,89 +0,0 @@
1
- /**
2
- * Premium Utilities Tests
3
- *
4
- * Tests for premium status fetching and async functions
5
- */
6
-
7
- import { getIsPremium } from '../premiumStatusUtils';
8
- import type { PremiumStatusFetcher } from '../types';
9
-
10
- describe('getIsPremium', () => {
11
- describe('Sync mode (boolean isPremium)', () => {
12
- it('should return false for guest users', () => {
13
- const result = getIsPremium(true, null, true);
14
- expect(result).toBe(false);
15
- });
16
-
17
- it('should return false when userId is null', () => {
18
- const result = getIsPremium(false, null, true);
19
- expect(result).toBe(false);
20
- });
21
-
22
- it('should return true when isPremium is true', () => {
23
- const result = getIsPremium(false, 'user123', true);
24
- expect(result).toBe(true);
25
- });
26
-
27
- it('should return false when isPremium is false', () => {
28
- const result = getIsPremium(false, 'user123', false);
29
- expect(result).toBe(false);
30
- });
31
- });
32
-
33
- describe('Async mode (fetcher)', () => {
34
- const mockFetcher: PremiumStatusFetcher = {
35
- isPremium: jest.fn(),
36
- };
37
-
38
- beforeEach(() => {
39
- jest.clearAllMocks();
40
- });
41
-
42
- it('should return false for guest users without calling fetcher', async () => {
43
- const result = await getIsPremium(true, null, mockFetcher);
44
- expect(result).toBe(false);
45
- expect(mockFetcher.isPremium).not.toHaveBeenCalled();
46
- });
47
-
48
- it('should return false when userId is null without calling fetcher', async () => {
49
- const result = await getIsPremium(false, null, mockFetcher);
50
- expect(result).toBe(false);
51
- expect(mockFetcher.isPremium).not.toHaveBeenCalled();
52
- });
53
-
54
- it('should call fetcher for authenticated users', async () => {
55
- (mockFetcher.isPremium as jest.Mock).mockResolvedValue(true);
56
-
57
- const result = await getIsPremium(false, 'user123', mockFetcher);
58
- expect(result).toBe(true);
59
- expect(mockFetcher.isPremium).toHaveBeenCalledWith('user123');
60
- expect(mockFetcher.isPremium).toHaveBeenCalledTimes(1);
61
- });
62
-
63
- it('should return false when fetcher returns false', async () => {
64
- (mockFetcher.isPremium as jest.Mock).mockResolvedValue(false);
65
-
66
- const result = await getIsPremium(false, 'user123', mockFetcher);
67
- expect(result).toBe(false);
68
- expect(mockFetcher.isPremium).toHaveBeenCalledWith('user123');
69
- });
70
-
71
- it('should throw error when fetcher throws Error', async () => {
72
- const error = new Error('Database error');
73
- (mockFetcher.isPremium as jest.Mock).mockRejectedValue(error);
74
-
75
- await expect(getIsPremium(false, 'user123', mockFetcher)).rejects.toThrow(
76
- 'Failed to fetch premium status: Database error'
77
- );
78
- });
79
-
80
- it('should throw error when fetcher throws non-Error', async () => {
81
- const error = 'String error';
82
- (mockFetcher.isPremium as jest.Mock).mockRejectedValue(error);
83
-
84
- await expect(getIsPremium(false, 'user123', mockFetcher)).rejects.toThrow(
85
- 'Failed to fetch premium status: String error'
86
- );
87
- });
88
- });
89
- });
@@ -1,74 +0,0 @@
1
- /**
2
- * Tier Utilities Tests
3
- *
4
- * Tests for tier determination and comparison functions
5
- */
6
-
7
- import { getUserTierInfo, checkPremiumAccess } from '../tierUtils';
8
-
9
- describe('getUserTierInfo', () => {
10
- describe('Guest users', () => {
11
- it('should return guest tier when isGuest is true', () => {
12
- const result = getUserTierInfo(true, null, false);
13
- expect(result.tier).toBe('guest');
14
- expect(result.isPremium).toBe(false);
15
- expect(result.isGuest).toBe(true);
16
- expect(result.isAuthenticated).toBe(false);
17
- expect(result.userId).toBe(null);
18
- });
19
-
20
- it('should return guest tier when userId is null', () => {
21
- const result = getUserTierInfo(false, null, false);
22
- expect(result.tier).toBe('guest');
23
- expect(result.isPremium).toBe(false);
24
- expect(result.isGuest).toBe(true);
25
- expect(result.isAuthenticated).toBe(false);
26
- expect(result.userId).toBe(null);
27
- });
28
-
29
- it('should ignore isPremium for guest users', () => {
30
- const result = getUserTierInfo(true, null, true);
31
- expect(result.tier).toBe('guest');
32
- expect(result.isPremium).toBe(false);
33
- });
34
- });
35
-
36
- describe('Authenticated users', () => {
37
- it('should return premium tier for authenticated premium users', () => {
38
- const result = getUserTierInfo(false, 'user123', true);
39
- expect(result.tier).toBe('premium');
40
- expect(result.isPremium).toBe(true);
41
- expect(result.isGuest).toBe(false);
42
- expect(result.isAuthenticated).toBe(true);
43
- expect(result.userId).toBe('user123');
44
- });
45
-
46
- it('should return freemium tier for authenticated non-premium users', () => {
47
- const result = getUserTierInfo(false, 'user123', false);
48
- expect(result.tier).toBe('freemium');
49
- expect(result.isPremium).toBe(false);
50
- expect(result.isGuest).toBe(false);
51
- expect(result.isAuthenticated).toBe(true);
52
- expect(result.userId).toBe('user123');
53
- });
54
- });
55
- });
56
-
57
- describe('checkPremiumAccess', () => {
58
- it('should return false for guest users', () => {
59
- expect(checkPremiumAccess(true, null, true)).toBe(false);
60
- expect(checkPremiumAccess(true, null, false)).toBe(false);
61
- });
62
-
63
- it('should return false when userId is null', () => {
64
- expect(checkPremiumAccess(false, null, true)).toBe(false);
65
- });
66
-
67
- it('should return true for authenticated premium users', () => {
68
- expect(checkPremiumAccess(false, 'user123', true)).toBe(true);
69
- });
70
-
71
- it('should return false for authenticated freemium users', () => {
72
- expect(checkPremiumAccess(false, 'user123', false)).toBe(false);
73
- });
74
- });
@@ -1,105 +0,0 @@
1
- /**
2
- * User Tier Validation Tests
3
- *
4
- * Tests for validation functions and type guards
5
- */
6
-
7
- import {
8
- isValidUserTier,
9
- isUserTierInfo,
10
- validateUserId,
11
- validateIsGuest,
12
- validateIsPremium,
13
- validateFetcher,
14
- } from '../validation';
15
- import type { UserTierInfo, PremiumStatusFetcher } from '../types';
16
-
17
- describe('isValidUserTier', () => {
18
- it('should return true for valid tiers', () => {
19
- expect(isValidUserTier('guest')).toBe(true);
20
- expect(isValidUserTier('freemium')).toBe(true);
21
- expect(isValidUserTier('premium')).toBe(true);
22
- });
23
-
24
- it('should return false for invalid values', () => {
25
- expect(isValidUserTier('invalid')).toBe(false);
26
- expect(isValidUserTier('')).toBe(false);
27
- expect(isValidUserTier(null)).toBe(false);
28
- expect(isValidUserTier(undefined)).toBe(false);
29
- expect(isValidUserTier(123)).toBe(false);
30
- expect(isValidUserTier({})).toBe(false);
31
- });
32
- });
33
-
34
- describe('isUserTierInfo', () => {
35
- it('should return true for valid UserTierInfo', () => {
36
- const validInfo: UserTierInfo = {
37
- tier: 'premium',
38
- isPremium: true,
39
- isGuest: false,
40
- isAuthenticated: true,
41
- userId: 'user123',
42
- };
43
- expect(isUserTierInfo(validInfo)).toBe(true);
44
- });
45
-
46
- it('should return false for invalid objects', () => {
47
- expect(isUserTierInfo(null)).toBe(false);
48
- expect(isUserTierInfo(undefined)).toBe(false);
49
- expect(isUserTierInfo('string')).toBe(false);
50
- expect(isUserTierInfo({})).toBe(false);
51
- expect(isUserTierInfo({ tier: 'invalid' })).toBe(false);
52
- expect(isUserTierInfo({ tier: 'premium' })).toBe(false);
53
- });
54
- });
55
-
56
- describe('validateUserId', () => {
57
- it('should not throw for valid userId', () => {
58
- expect(() => validateUserId('user123')).not.toThrow();
59
- expect(() => validateUserId(null)).not.toThrow();
60
- });
61
-
62
- it('should throw for invalid userId', () => {
63
- expect(() => validateUserId('')).toThrow(TypeError);
64
- expect(() => validateUserId(' ')).toThrow(TypeError);
65
- });
66
- });
67
-
68
- describe('validateIsGuest', () => {
69
- it('should not throw for valid isGuest', () => {
70
- expect(() => validateIsGuest(true)).not.toThrow();
71
- expect(() => validateIsGuest(false)).not.toThrow();
72
- });
73
-
74
- it('should throw for invalid isGuest', () => {
75
- expect(() => validateIsGuest('true' as unknown as boolean)).toThrow(TypeError);
76
- expect(() => validateIsGuest(1 as unknown as boolean)).toThrow(TypeError);
77
- });
78
- });
79
-
80
- describe('validateIsPremium', () => {
81
- it('should not throw for valid isPremium', () => {
82
- expect(() => validateIsPremium(true)).not.toThrow();
83
- expect(() => validateIsPremium(false)).not.toThrow();
84
- });
85
-
86
- it('should throw for invalid isPremium', () => {
87
- expect(() => validateIsPremium('true' as unknown as boolean)).toThrow(TypeError);
88
- expect(() => validateIsPremium(1 as unknown as boolean)).toThrow(TypeError);
89
- });
90
- });
91
-
92
- describe('validateFetcher', () => {
93
- it('should not throw for valid fetcher', () => {
94
- const validFetcher: PremiumStatusFetcher = {
95
- isPremium: async () => true,
96
- };
97
- expect(() => validateFetcher(validFetcher)).not.toThrow();
98
- });
99
-
100
- it('should throw for invalid fetcher', () => {
101
- expect(() => validateFetcher(null as unknown as PremiumStatusFetcher)).toThrow(TypeError);
102
- expect(() => validateFetcher({} as unknown as PremiumStatusFetcher)).toThrow(TypeError);
103
- expect(() => validateFetcher({ isPremium: 'not a function' } as unknown as PremiumStatusFetcher)).toThrow(TypeError);
104
- });
105
- });
@@ -1,113 +0,0 @@
1
- /**
2
- * AI Credit Helpers
3
- *
4
- * Common patterns for AI generation apps to handle credits.
5
- * Provides ready-to-use functions for credit checking and deduction.
6
- *
7
- * Usage:
8
- * import { createAICreditHelpers } from '@umituz/react-native-subscription';
9
- *
10
- * const helpers = createAICreditHelpers({
11
- * repository,
12
- * imageGenerationTypes: ['future_image', 'santa_transform'],
13
- * onCreditDeducted: (userId) => invalidateCache(userId)
14
- * });
15
- */
16
-
17
- import type { CreditsRepository } from "../domains/credits/infrastructure/CreditsRepository";
18
- import { createCreditChecker } from "./creditChecker";
19
-
20
- export interface AICreditHelpersConfig {
21
- /**
22
- * Credits repository instance
23
- */
24
- repository: CreditsRepository;
25
-
26
- /**
27
- * Optional map of operation types to credit costs.
28
- * If an operation isn't in this map, cost defaults to 1.
29
- * @example { 'high_res_image': 5, 'text_summary': 1 }
30
- */
31
- operationCosts?: Record<string, number>;
32
-
33
- /**
34
- * Optional callback called after successful credit deduction.
35
- * Use this to invalidate TanStack Query cache or trigger UI updates.
36
- */
37
- onCreditDeducted?: (userId: string, cost: number) => void;
38
- }
39
-
40
- export interface AICreditHelpers {
41
- /**
42
- * Check if user has credits for a specific generation type
43
- * @param userId - User ID
44
- * @param generationType - Type of generation
45
- * @returns boolean indicating if credits are available
46
- */
47
- checkCreditsForGeneration: (
48
- userId: string | undefined,
49
- generationType: string
50
- ) => Promise<boolean>;
51
-
52
- /**
53
- * Deduct credits after successful generation
54
- * @param userId - User ID
55
- * @param generationType - Type of generation that was performed
56
- */
57
- deductCreditsForGeneration: (
58
- userId: string | undefined,
59
- generationType: string
60
- ) => Promise<void>;
61
-
62
- /**
63
- * Get cost for a generation type
64
- * @param generationType - Type of generation
65
- * @returns number of credits
66
- */
67
- getCost: (generationType: string) => number;
68
- }
69
-
70
- /**
71
- * Creates AI-specific credit helper functions
72
- */
73
- export function createAICreditHelpers(
74
- config: AICreditHelpersConfig
75
- ): AICreditHelpers {
76
- const { repository, operationCosts = {}, onCreditDeducted } = config;
77
-
78
- // Map generation type to cost
79
- const getCost = (generationType: string): number => {
80
- return operationCosts[generationType] ?? 1;
81
- };
82
-
83
- // Create credit checker
84
- const checker = createCreditChecker({
85
- repository,
86
- onCreditDeducted,
87
- });
88
-
89
- // Check if credits are available for generation
90
- const checkCreditsForGeneration = async (
91
- userId: string | undefined,
92
- generationType: string
93
- ): Promise<boolean> => {
94
- const cost = getCost(generationType);
95
- const result = await checker.checkCreditsAvailable(userId, cost);
96
- return result.success;
97
- };
98
-
99
- // Deduct credits after successful generation
100
- const deductCreditsForGeneration = async (
101
- userId: string | undefined,
102
- generationType: string
103
- ): Promise<void> => {
104
- const cost = getCost(generationType);
105
- await checker.deductCreditsAfterSuccess(userId, cost);
106
- };
107
-
108
- return {
109
- checkCreditsForGeneration,
110
- deductCreditsForGeneration,
111
- getCost,
112
- };
113
- }
@@ -1,19 +0,0 @@
1
- /**
2
- * Authentication Utilities
3
- *
4
- * Centralized logic for authentication checks
5
- */
6
-
7
- export function isAuthenticated(
8
- isGuest: boolean,
9
- userId: string | null,
10
- ): boolean {
11
- return !isGuest && userId !== null;
12
- }
13
-
14
- export function isGuest(
15
- isGuestFlag: boolean,
16
- userId: string | null,
17
- ): boolean {
18
- return isGuestFlag || userId === null;
19
- }
@@ -1,82 +0,0 @@
1
- /**
2
- * Credit Checker Utility
3
- *
4
- * Validates credit availability before operations.
5
- * Generic - works with any generation type mapping.
6
- */
7
-
8
- import type { CreditsRepository } from "../domains/credits/infrastructure/CreditsRepository";
9
-
10
- export interface CreditCheckResult {
11
- success: boolean;
12
- error?: string;
13
- }
14
-
15
- export interface CreditCheckerConfig {
16
- repository: CreditsRepository;
17
- /**
18
- * Optional callback called after successful credit deduction.
19
- * Use this to invalidate TanStack Query cache or trigger UI updates.
20
- * @param userId - The user whose credits were deducted
21
- * @param cost - The amount of credits deducted
22
- */
23
- onCreditDeducted?: (userId: string, cost: number) => void;
24
- }
25
-
26
- export const createCreditChecker = (config: CreditCheckerConfig) => {
27
- const { repository, onCreditDeducted } = config;
28
-
29
- const checkCreditsAvailable = async (
30
- userId: string | undefined,
31
- cost: number = 1
32
- ): Promise<CreditCheckResult> => {
33
- if (!userId) {
34
- return { success: false, error: "anonymous_user_blocked" };
35
- }
36
-
37
- const hasCreditsAvailable = await repository.hasCredits(userId, cost);
38
-
39
- if (!hasCreditsAvailable) {
40
- return {
41
- success: false,
42
- error: "credits_exhausted",
43
- };
44
- }
45
-
46
- return { success: true };
47
- };
48
-
49
- const deductCreditsAfterSuccess = async (
50
- userId: string | undefined,
51
- cost: number = 1
52
- ): Promise<void> => {
53
- if (!userId) return;
54
-
55
- const maxRetries = 3;
56
- let lastError: Error | null = null;
57
-
58
- for (let attempt = 0; attempt < maxRetries; attempt++) {
59
- const result = await repository.deductCredit(userId, cost);
60
- if (result.success) {
61
- // Notify subscribers that credits were deducted
62
- onCreditDeducted?.(userId, cost);
63
- return;
64
- }
65
- lastError = new Error(result.error?.message || "Deduction failed");
66
- await new Promise<void>((r) => setTimeout(() => r(), 500 * (attempt + 1)));
67
- }
68
-
69
- if (lastError) {
70
- throw lastError;
71
- }
72
- };
73
-
74
-
75
- return {
76
- checkCreditsAvailable,
77
- deductCreditsAfterSuccess,
78
- };
79
- };
80
-
81
- export type CreditChecker = ReturnType<typeof createCreditChecker>;
82
-