@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.
- package/package.json +1 -1
- package/src/domains/credits/presentation/useCredits.ts +0 -18
- package/src/domains/credits/presentation/useDeductCredit.ts +7 -8
- package/src/domains/credits/utils/creditCalculations.ts +7 -0
- package/src/domains/subscription/core/RevenueCatTypes.ts +2 -1
- package/src/domains/subscription/core/SubscriptionConstants.ts +12 -0
- package/src/index.ts +5 -5
- package/src/presentation/hooks/README.md +1 -1
- package/src/presentation/hooks/usePremium.md +2 -2
- package/src/utils/index.ts +1 -3
- package/src/utils/packageTypeDetector.ts +8 -18
- package/src/utils/premiumStatusUtils.ts +4 -4
- package/src/utils/tierUtils.ts +10 -10
- package/src/utils/types.ts +7 -4
- package/src/utils/validation.ts +3 -3
- package/src/domains/config/README.md +0 -100
- package/src/utils/README.md +0 -42
- package/src/utils/__tests__/authUtils.test.ts +0 -37
- package/src/utils/__tests__/edgeCases.test.ts +0 -79
- package/src/utils/__tests__/premiumUtils.test.ts +0 -89
- package/src/utils/__tests__/tierUtils.test.ts +0 -74
- package/src/utils/__tests__/validation.test.ts +0 -105
- package/src/utils/aiCreditHelpers.ts +0 -113
- package/src/utils/authUtils.ts +0 -19
- package/src/utils/creditChecker.ts +0 -82
- /package/src/{presentation → domains/subscription/presentation}/components/README.md +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/details/CreditRow.md +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/details/CreditRow.tsx +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/details/DetailRow.md +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/details/DetailRow.tsx +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/details/PremiumDetailsCard.md +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/details/PremiumDetailsCard.styles.ts +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/details/PremiumDetailsCard.tsx +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/details/PremiumDetailsCardTypes.ts +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/details/PremiumStatusBadge.md +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/details/PremiumStatusBadge.tsx +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/details/README.md +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/feedback/PaywallFeedbackModal.md +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/feedback/PaywallFeedbackModal.tsx +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/feedback/README.md +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/feedback/paywallFeedbackStyles.ts +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/overlay/PurchaseLoadingOverlay.tsx +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/overlay/index.ts +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/paywall/PaywallModal.md +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/paywall/README.md +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/sections/README.md +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/sections/SubscriptionSection.md +0 -0
- /package/src/{presentation → domains/subscription/presentation}/components/sections/SubscriptionSection.tsx +0 -0
- /package/src/{presentation → domains/subscription/presentation}/screens/README.md +0 -0
- /package/src/{presentation → domains/subscription/presentation}/screens/SubscriptionDetailScreen.tsx +0 -0
- /package/src/{presentation → domains/subscription/presentation}/screens/components/CreditsList.tsx +0 -0
- /package/src/{presentation → domains/subscription/presentation}/screens/components/DevTestSection.tsx +0 -0
- /package/src/{presentation → domains/subscription/presentation}/screens/components/SubscriptionActions.tsx +0 -0
- /package/src/{presentation → domains/subscription/presentation}/screens/components/SubscriptionHeader.tsx +0 -0
- /package/src/{presentation → domains/subscription/presentation}/screens/components/UpgradePrompt.tsx +0 -0
- /package/src/{presentation → domains/subscription/presentation}/stores/index.ts +0 -0
- /package/src/{presentation → domains/subscription/presentation}/stores/purchaseLoadingStore.ts +0 -0
- /package/src/{presentation → domains/subscription/presentation}/types/README.md +0 -0
- /package/src/{presentation → domains/subscription/presentation}/types/SubscriptionDetailTypes.ts +0 -0
- /package/src/{presentation → domains/subscription/presentation}/types/SubscriptionSettingsTypes.ts +0 -0
- /package/src/{presentation → domains/subscription/presentation}/utils/README.md +0 -0
- /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.
|
|
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
|
-
//
|
|
51
|
-
|
|
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
|
-
//
|
|
71
|
-
//
|
|
72
|
-
if (userId && context?.previousCredits && !context.skippedOptimistic
|
|
73
|
-
|
|
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: () => {
|
|
@@ -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 =
|
|
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 (
|
|
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
|
|
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
|
|
70
|
+
- [ ] Test with anonymous user
|
|
71
71
|
- [ ] Test offline scenario
|
|
72
72
|
|
|
73
73
|
### Common Patterns
|
package/src/utils/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
8
|
+
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Get isPremium value with centralized logic
|
|
12
12
|
*/
|
|
13
13
|
export function getIsPremium(
|
|
14
|
-
|
|
14
|
+
isAnonymous: boolean,
|
|
15
15
|
userId: string | null,
|
|
16
16
|
isPremiumOrFetcher: boolean | PremiumStatusFetcher,
|
|
17
17
|
): Promise<boolean> {
|
|
18
|
-
//
|
|
19
|
-
if (
|
|
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);
|
package/src/utils/tierUtils.ts
CHANGED
|
@@ -4,38 +4,38 @@
|
|
|
4
4
|
* Core logic for determining user tier and premium status
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type
|
|
8
|
-
|
|
7
|
+
import { USER_TIER, type UserTierInfo } from './types';
|
|
8
|
+
|
|
9
9
|
|
|
10
10
|
export function getUserTierInfo(
|
|
11
|
-
|
|
11
|
+
isAnonymous: boolean,
|
|
12
12
|
userId: string | null,
|
|
13
13
|
isPremium: boolean,
|
|
14
14
|
): UserTierInfo {
|
|
15
|
-
if (
|
|
15
|
+
if (isAnonymous || userId === null) {
|
|
16
16
|
return {
|
|
17
|
-
tier:
|
|
17
|
+
tier: USER_TIER.ANONYMOUS,
|
|
18
18
|
isPremium: false,
|
|
19
|
-
|
|
19
|
+
isAnonymous: true,
|
|
20
20
|
isAuthenticated: false,
|
|
21
21
|
userId: null,
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
return {
|
|
26
|
-
tier: isPremium ?
|
|
26
|
+
tier: isPremium ? USER_TIER.PREMIUM : USER_TIER.FREEMIUM,
|
|
27
27
|
isPremium,
|
|
28
|
-
|
|
28
|
+
isAnonymous: false,
|
|
29
29
|
isAuthenticated: true,
|
|
30
30
|
userId,
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export function checkPremiumAccess(
|
|
35
|
-
|
|
35
|
+
isAnonymous: boolean,
|
|
36
36
|
userId: string | null,
|
|
37
37
|
isPremium: boolean,
|
|
38
38
|
): boolean {
|
|
39
|
-
if (
|
|
39
|
+
if (isAnonymous || userId === null) return false;
|
|
40
40
|
return isPremium;
|
|
41
41
|
}
|
package/src/utils/types.ts
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* Type definitions for user tier system
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
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
|
|
17
|
-
|
|
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
|
|
25
|
+
/** User ID (null for anonymous users) */
|
|
23
26
|
userId: string | null;
|
|
24
27
|
}
|
|
25
28
|
|
package/src/utils/validation.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* Type guards and validation functions for user tier system
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type
|
|
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 ===
|
|
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.
|
|
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)
|
package/src/utils/README.md
DELETED
|
@@ -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
|
-
}
|
package/src/utils/authUtils.ts
DELETED
|
@@ -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
|
-
|
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/components/details/CreditRow.md
RENAMED
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/components/details/CreditRow.tsx
RENAMED
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/components/details/DetailRow.md
RENAMED
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/components/details/DetailRow.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/components/details/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/components/feedback/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/components/paywall/PaywallModal.md
RENAMED
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/components/paywall/README.md
RENAMED
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/components/sections/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/screens/SubscriptionDetailScreen.tsx
RENAMED
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/screens/components/CreditsList.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/screens/components/UpgradePrompt.tsx
RENAMED
|
File without changes
|
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/stores/purchaseLoadingStore.ts
RENAMED
|
File without changes
|
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/types/SubscriptionDetailTypes.ts
RENAMED
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/types/SubscriptionSettingsTypes.ts
RENAMED
|
File without changes
|
|
File without changes
|
/package/src/{presentation → domains/subscription/presentation}/utils/subscriptionDateUtils.ts
RENAMED
|
File without changes
|