@umituz/react-native-subscription 2.27.89 → 2.27.92

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 (62) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/presentation/useCredits.ts +0 -18
  3. package/src/domains/credits/presentation/useDeductCredit.ts +7 -8
  4. package/src/domains/credits/utils/creditCalculations.ts +7 -0
  5. package/src/domains/subscription/core/RevenueCatTypes.ts +2 -1
  6. package/src/domains/subscription/core/SubscriptionConstants.ts +12 -0
  7. package/src/index.ts +5 -5
  8. package/src/presentation/hooks/README.md +1 -1
  9. package/src/presentation/hooks/usePremium.md +2 -2
  10. package/src/utils/index.ts +1 -3
  11. package/src/utils/packageTypeDetector.ts +8 -18
  12. package/src/utils/premiumStatusUtils.ts +4 -4
  13. package/src/utils/tierUtils.ts +10 -10
  14. package/src/utils/types.ts +7 -4
  15. package/src/utils/validation.ts +3 -3
  16. package/src/domains/config/README.md +0 -100
  17. package/src/utils/README.md +0 -42
  18. package/src/utils/__tests__/authUtils.test.ts +0 -37
  19. package/src/utils/__tests__/edgeCases.test.ts +0 -79
  20. package/src/utils/__tests__/premiumUtils.test.ts +0 -89
  21. package/src/utils/__tests__/tierUtils.test.ts +0 -74
  22. package/src/utils/__tests__/validation.test.ts +0 -105
  23. package/src/utils/aiCreditHelpers.ts +0 -113
  24. package/src/utils/authUtils.ts +0 -19
  25. package/src/utils/creditChecker.ts +0 -82
  26. /package/src/{presentation → domains/subscription/presentation}/components/README.md +0 -0
  27. /package/src/{presentation → domains/subscription/presentation}/components/details/CreditRow.md +0 -0
  28. /package/src/{presentation → domains/subscription/presentation}/components/details/CreditRow.tsx +0 -0
  29. /package/src/{presentation → domains/subscription/presentation}/components/details/DetailRow.md +0 -0
  30. /package/src/{presentation → domains/subscription/presentation}/components/details/DetailRow.tsx +0 -0
  31. /package/src/{presentation → domains/subscription/presentation}/components/details/PremiumDetailsCard.md +0 -0
  32. /package/src/{presentation → domains/subscription/presentation}/components/details/PremiumDetailsCard.styles.ts +0 -0
  33. /package/src/{presentation → domains/subscription/presentation}/components/details/PremiumDetailsCard.tsx +0 -0
  34. /package/src/{presentation → domains/subscription/presentation}/components/details/PremiumDetailsCardTypes.ts +0 -0
  35. /package/src/{presentation → domains/subscription/presentation}/components/details/PremiumStatusBadge.md +0 -0
  36. /package/src/{presentation → domains/subscription/presentation}/components/details/PremiumStatusBadge.tsx +0 -0
  37. /package/src/{presentation → domains/subscription/presentation}/components/details/README.md +0 -0
  38. /package/src/{presentation → domains/subscription/presentation}/components/feedback/PaywallFeedbackModal.md +0 -0
  39. /package/src/{presentation → domains/subscription/presentation}/components/feedback/PaywallFeedbackModal.tsx +0 -0
  40. /package/src/{presentation → domains/subscription/presentation}/components/feedback/README.md +0 -0
  41. /package/src/{presentation → domains/subscription/presentation}/components/feedback/paywallFeedbackStyles.ts +0 -0
  42. /package/src/{presentation → domains/subscription/presentation}/components/overlay/PurchaseLoadingOverlay.tsx +0 -0
  43. /package/src/{presentation → domains/subscription/presentation}/components/overlay/index.ts +0 -0
  44. /package/src/{presentation → domains/subscription/presentation}/components/paywall/PaywallModal.md +0 -0
  45. /package/src/{presentation → domains/subscription/presentation}/components/paywall/README.md +0 -0
  46. /package/src/{presentation → domains/subscription/presentation}/components/sections/README.md +0 -0
  47. /package/src/{presentation → domains/subscription/presentation}/components/sections/SubscriptionSection.md +0 -0
  48. /package/src/{presentation → domains/subscription/presentation}/components/sections/SubscriptionSection.tsx +0 -0
  49. /package/src/{presentation → domains/subscription/presentation}/screens/README.md +0 -0
  50. /package/src/{presentation → domains/subscription/presentation}/screens/SubscriptionDetailScreen.tsx +0 -0
  51. /package/src/{presentation → domains/subscription/presentation}/screens/components/CreditsList.tsx +0 -0
  52. /package/src/{presentation → domains/subscription/presentation}/screens/components/DevTestSection.tsx +0 -0
  53. /package/src/{presentation → domains/subscription/presentation}/screens/components/SubscriptionActions.tsx +0 -0
  54. /package/src/{presentation → domains/subscription/presentation}/screens/components/SubscriptionHeader.tsx +0 -0
  55. /package/src/{presentation → domains/subscription/presentation}/screens/components/UpgradePrompt.tsx +0 -0
  56. /package/src/{presentation → domains/subscription/presentation}/stores/index.ts +0 -0
  57. /package/src/{presentation → domains/subscription/presentation}/stores/purchaseLoadingStore.ts +0 -0
  58. /package/src/{presentation → domains/subscription/presentation}/types/README.md +0 -0
  59. /package/src/{presentation → domains/subscription/presentation}/types/SubscriptionDetailTypes.ts +0 -0
  60. /package/src/{presentation → domains/subscription/presentation}/types/SubscriptionSettingsTypes.ts +0 -0
  61. /package/src/{presentation → domains/subscription/presentation}/utils/README.md +0 -0
  62. /package/src/{presentation → domains/subscription/presentation}/utils/subscriptionDateUtils.ts +0 -0
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.92",
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
+ };
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { CustomerInfo } from "react-native-purchases";
7
+ import { DEFAULT_ENTITLEMENT_ID } from "./SubscriptionConstants";
7
8
 
8
9
  /**
9
10
  * RevenueCat Entitlement Info
@@ -35,7 +36,7 @@ export interface RevenueCatPurchaseErrorInfo extends Error {
35
36
  */
36
37
  export function getPremiumEntitlement(
37
38
  customerInfo: CustomerInfo,
38
- entitlementIdentifier: string = 'premium'
39
+ entitlementIdentifier: string = DEFAULT_ENTITLEMENT_ID
39
40
  ): RevenueCatEntitlement | null {
40
41
  const entitlement = customerInfo.entitlements.active[entitlementIdentifier];
41
42
  if (!entitlement) {
@@ -3,6 +3,18 @@
3
3
  * Centralized source of truth for subscription-related enums and types.
4
4
  */
5
5
 
6
+ /** User tier constants */
7
+ export const USER_TIER = {
8
+ ANONYMOUS: 'anonymous',
9
+ FREEMIUM: 'freemium',
10
+ PREMIUM: 'premium',
11
+ } as const;
12
+
13
+ export type UserTierType = (typeof USER_TIER)[keyof typeof USER_TIER];
14
+
15
+ /** Default entitlement identifier */
16
+ export const DEFAULT_ENTITLEMENT_ID = 'premium';
17
+
6
18
  /** Subscription status constants */
7
19
  export const SUBSCRIPTION_STATUS = {
8
20
  ACTIVE: 'active',
package/src/index.ts CHANGED
@@ -54,11 +54,11 @@ export {
54
54
  export * from "./presentation/hooks";
55
55
 
56
56
  // Presentation Layer - Components
57
- export * from "./presentation/components/details/PremiumDetailsCard";
58
- export * from "./presentation/components/details/PremiumStatusBadge";
59
- export * from "./presentation/components/sections/SubscriptionSection";
60
- export * from "./presentation/components/feedback/PaywallFeedbackModal";
61
- export * from "./presentation/screens/SubscriptionDetailScreen";
57
+ export * from "./domains/subscription/presentation/components/details/PremiumDetailsCard";
58
+ export * from "./domains/subscription/presentation/components/details/PremiumStatusBadge";
59
+ export * from "./domains/subscription/presentation/components/sections/SubscriptionSection";
60
+ export * from "./domains/subscription/presentation/components/feedback/PaywallFeedbackModal";
61
+ export * from "./domains/subscription/presentation/screens/SubscriptionDetailScreen";
62
62
  export * from "./domains/paywall/components/PaywallContainer";
63
63
 
64
64
  export type {
@@ -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);
@@ -4,38 +4,38 @@
4
4
  * Core logic for determining user tier and premium status
5
5
  */
6
6
 
7
- import type { UserTierInfo } from './types';
8
- import { isGuest } from './authUtils';
7
+ import { USER_TIER, type UserTierInfo } from './types';
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: USER_TIER.ANONYMOUS,
18
18
  isPremium: false,
19
- isGuest: true,
19
+ isAnonymous: true,
20
20
  isAuthenticated: false,
21
21
  userId: null,
22
22
  };
23
23
  }
24
24
 
25
25
  return {
26
- tier: isPremium ? 'premium' : 'freemium',
26
+ tier: isPremium ? USER_TIER.PREMIUM : USER_TIER.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,10 @@
4
4
  * Type definitions for user tier system
5
5
  */
6
6
 
7
- export type UserTier = 'guest' | 'freemium' | 'premium';
7
+ import { USER_TIER, type UserTierType } from '../domains/subscription/core/SubscriptionConstants';
8
+
9
+ export type UserTier = UserTierType;
10
+ export { USER_TIER };
8
11
 
9
12
  export interface UserTierInfo {
10
13
  /** User tier classification */
@@ -13,13 +16,13 @@ export interface UserTierInfo {
13
16
  /** Whether user has premium access */
14
17
  isPremium: boolean;
15
18
 
16
- /** Whether user is a guest (not authenticated) */
17
- isGuest: boolean;
19
+ /** Whether user is anonymous (not authenticated) */
20
+ isAnonymous: boolean;
18
21
 
19
22
  /** Whether user is authenticated */
20
23
  isAuthenticated: boolean;
21
24
 
22
- /** User ID (null for guests) */
25
+ /** User ID (null for anonymous users) */
23
26
  userId: string | null;
24
27
  }
25
28
 
@@ -4,10 +4,10 @@
4
4
  * Type guards and validation functions for user tier system
5
5
  */
6
6
 
7
- import type { UserTier, UserTierInfo } from './types';
7
+ import { USER_TIER, type UserTier, type 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 === USER_TIER.ANONYMOUS || value === USER_TIER.FREEMIUM || value === USER_TIER.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,100 +0,0 @@
1
- # Config Domain
2
-
3
- Central configuration system for subscription plans, product configurations, and package management.
4
-
5
- ## Location
6
-
7
- - **Base Path**: `/Users/umituz/Desktop/github/umituz/apps/artificial_intelligence/npm-packages/react-native-subscription/src/domains/config/`
8
- - **Domain**: `src/domains/config/domain/`
9
- - **Entities**: `src/domains/config/domain/entities/`
10
-
11
- ## Strategy
12
-
13
- ### Plan Management
14
-
15
- Comprehensive subscription plan configuration system.
16
-
17
- - **Plan Types**: Monthly, annual, and lifetime plan configurations
18
- - **Product Metadata**: RevenueCat product metadata management
19
- - **Validation**: Configuration validation and type safety
20
- - **Helper Functions**: Plan comparison and filtering utilities
21
-
22
- ### Configuration Objects
23
-
24
- Structured configuration for different aspects of the system.
25
-
26
- - **SubscriptionConfig**: Main subscription configuration
27
- - **WalletConfig**: Credit system configuration
28
- - **Plan Entities**: Individual plan definitions
29
- - **Validation Rules**: Configuration validation schemas
30
-
31
- ### Helper Utilities
32
-
33
- Plan comparison and manipulation functions.
34
-
35
- - **Plan Comparison**: Value comparison between plans
36
- - **Price Formatting**: Currency-aware price formatting
37
- - **Discount Calculation**: Savings and discount calculations
38
- - **Package Filtering**: Type-based package filtering
39
-
40
- ### Validation System
41
-
42
- Comprehensive configuration validation.
43
-
44
- - **Plan Validation**: Plan entity validation rules
45
- - **Config Validation**: Complete configuration validation
46
- - **Type Safety**: TypeScript type definitions
47
- - **Error Messages**: Detailed validation error reporting
48
-
49
- ## Restrictions
50
-
51
- ### REQUIRED
52
-
53
- - **Type Safety**: Always use TypeScript type definitions
54
- - **Validation**: Validate configurations before runtime use
55
- - **Default Values**: Provide meaningful default values
56
- - **Immutable Updates**: Create new copies instead of modifying
57
-
58
- ### PROHIBITED
59
-
60
- - **Invalid Prices**: Negative or zero prices not allowed
61
- - **Missing IDs**: All plans must have valid IDs
62
- - **Duplicate Plans**: No duplicate plan IDs allowed
63
- - **Hardcoded Values**: Use configuration system
64
-
65
- ### CRITICAL
66
-
67
- - **Configuration Integrity**: All configurations must be valid
68
- - **Plan Consistency**: Related plans must be consistent
69
- - **Currency Handling**: Proper currency code usage
70
- - **Feature Lists**: Accurate feature mapping
71
-
72
- ## AI Agent Guidelines
73
-
74
- ### When Modifying Configuration System
75
-
76
- 1. **Type Definitions**: Update TypeScript types for new config
77
- 2. **Validation Rules**: Add validation for new fields
78
- 3. **Default Values**: Provide sensible defaults
79
- 4. **Documentation**: Document configuration options
80
-
81
- ### When Adding New Plan Types
82
-
83
- 1. **Entity Pattern**: Follow existing entity patterns
84
- 2. **Validation**: Add validation rules
85
- 3. **Helper Functions**: Create helper functions
86
- 4. **Testing**: Test with various configurations
87
-
88
- ### When Fixing Configuration Bugs
89
-
90
- 1. **Validation Logic**: Check validation rules
91
- 2. **Type Definitions**: Verify type correctness
92
- 3. **Default Values**: Ensure proper defaults
93
- 4. **Edge Cases**: Test boundary conditions
94
-
95
- ## Related Documentation
96
-
97
- - [Paywall Domain](/Users/umituz/Desktop/github/umituz/apps/artificial_intelligence/npm-packages/react-native-subscription/src/domains/paywall/README.md)
98
- - [Wallet Domain](/Users/umituz/Desktop/github/umituz/apps/artificial_intelligence/npm-packages/react-native-subscription/src/domains/wallet/README.md)
99
- - [RevenueCat Integration](/Users/umituz/Desktop/github/umituz/apps/artificial_intelligence/npm-packages/react-native-subscription/src/revenuecat/README.md)
100
- - [Domain Layer](/Users/umituz/Desktop/github/umituz/apps/artificial_intelligence/npm-packages/react-native-subscription/src/domain/README.md)
@@ -1,42 +0,0 @@
1
- # Utils
2
-
3
- ## Location
4
- Abonelik sistemi için yardımcı fonksiyonlar ve utility araçları.
5
-
6
- ## Strategy
7
- Bu dizin, premium durum kontrolü, kullanıcı tier yönetimi, paket işlemleri, fiyat hesaplama, periyot formatlama, asenkron işlemler, validasyon ve veri dönüşümü için yardımcı fonksiyonlar içerir.
8
-
9
- ## Restrictions
10
-
11
- ### REQUIRED
12
- - Must maintain type safety for all functions
13
- - Must handle null checks safely
14
- - Must support localization
15
- - Must validate all inputs
16
-
17
- ### PROHIBITED
18
- - DO NOT bypass null safety checks
19
- - DO NOT ignore error handling
20
- - DO NOT hardcode locale values
21
- - DO NOT skip validation
22
-
23
- ### CRITICAL SAFETY
24
- - All functions MUST be type-safe
25
- - Null checks MUST be performed safely
26
- - Errors MUST be caught and handled
27
- - Validation MUST return clear results
28
-
29
- ## AI Agent Guidelines
30
- 1. Maintain type safety for all utility functions
31
- 2. Perform null checks safely with proper guards
32
- 3. Catch and handle errors appropriately
33
- 4. Support localization for different locales
34
- 5. Write JSDoc comments for all functions
35
- 6. Test utility functions thoroughly with edge cases
36
- 7. Provide clear validation error messages
37
-
38
- ## Related Documentation
39
- - Premium utilities for status checks
40
- - User tier utilities for tier management
41
- - Package utilities for RevenueCat integration
42
- - Price utilities for formatting and calculation
@@ -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
-