@umituz/react-native-subscription 2.27.92 → 2.27.93
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/application/CreditsInitializer.ts +91 -38
- package/src/domains/credits/core/UserCreditsDocument.ts +33 -33
- package/src/domains/credits/infrastructure/CreditsRepository.ts +44 -58
- package/src/domains/paywall/components/PaywallModal.tsx +1 -1
- package/src/domains/subscription/application/SubscriptionInitializer.ts +59 -18
- package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +46 -27
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +106 -42
- package/src/domains/subscription/infrastructure/services/RestoreHandler.ts +4 -2
- package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +1 -2
- package/src/domains/subscription/infrastructure/utils/RenewalDetector.ts +1 -1
- package/src/domains/subscription/presentation/components/details/PremiumStatusBadge.tsx +6 -4
- package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackModal.tsx +1 -1
- package/src/domains/subscription/presentation/types/SubscriptionDetailTypes.ts +4 -2
- package/src/domains/subscription/presentation/types/SubscriptionSettingsTypes.ts +1 -1
- package/src/domains/subscription/presentation/usePremiumGate.ts +1 -1
- package/src/domains/subscription/presentation/useSavedPurchaseAutoExecution.ts +1 -1
- package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.ts +4 -3
- package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.utils.ts +1 -1
- package/src/domains/trial/application/TrialEligibilityService.ts +1 -1
- package/src/domains/trial/infrastructure/DeviceTrialRepository.ts +2 -2
- package/src/shared/application/ports/IRevenueCatService.ts +2 -0
- package/src/shared/infrastructure/SubscriptionEventBus.ts +5 -2
- package/src/presentation/README.md +0 -125
- package/src/presentation/hooks/README.md +0 -156
- package/src/presentation/hooks/useAuthSubscriptionSync.md +0 -94
- package/src/presentation/hooks/useCredits.md +0 -103
- package/src/presentation/hooks/useDeductCredit.md +0 -100
- package/src/presentation/hooks/useFeatureGate.md +0 -112
- package/src/presentation/hooks/usePaywall.md +0 -89
- package/src/presentation/hooks/usePaywallOperations.md +0 -92
- package/src/presentation/hooks/usePaywallVisibility.md +0 -95
- package/src/presentation/hooks/usePremium.md +0 -88
- package/src/presentation/hooks/useSubscriptionSettingsConfig.md +0 -94
|
@@ -14,7 +14,7 @@ import { SubscriptionInternalState } from "./SubscriptionInternalState";
|
|
|
14
14
|
export interface SubscriptionManagerConfig {
|
|
15
15
|
config: RevenueCatConfig;
|
|
16
16
|
apiKey: string;
|
|
17
|
-
getAnonymousUserId
|
|
17
|
+
getAnonymousUserId: () => Promise<string>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
class SubscriptionManagerImpl {
|
|
@@ -25,97 +25,161 @@ class SubscriptionManagerImpl {
|
|
|
25
25
|
|
|
26
26
|
configure(config: SubscriptionManagerConfig): void {
|
|
27
27
|
this.managerConfig = config;
|
|
28
|
-
this.
|
|
29
|
-
|
|
28
|
+
this.state.userIdProvider.configure(config.getAnonymousUserId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private ensurePackageHandlerInitialized(): void {
|
|
32
|
+
if (this.packageHandler) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!this.serviceInstance) {
|
|
37
|
+
throw new Error("Service instance not available");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!this.managerConfig) {
|
|
41
|
+
throw new Error("Manager not configured");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.packageHandler = new PackageHandler(
|
|
45
|
+
this.serviceInstance,
|
|
46
|
+
this.managerConfig.config.entitlementIdentifier
|
|
47
|
+
);
|
|
30
48
|
}
|
|
31
49
|
|
|
32
50
|
private ensureConfigured(): void {
|
|
33
|
-
if (!this.managerConfig
|
|
51
|
+
if (!this.managerConfig) {
|
|
52
|
+
throw new Error("SubscriptionManager not configured");
|
|
53
|
+
}
|
|
34
54
|
}
|
|
35
55
|
|
|
36
|
-
async initialize(userId
|
|
56
|
+
async initialize(userId: string): Promise<boolean> {
|
|
37
57
|
this.ensureConfigured();
|
|
38
|
-
const effectiveUserId = userId || (await this.state.userIdProvider.getOrCreateAnonymousUserId());
|
|
39
|
-
const { shouldInit, existingPromise } = this.state.initCache.tryAcquireInitialization(effectiveUserId);
|
|
40
58
|
|
|
41
|
-
|
|
59
|
+
const { shouldInit, existingPromise } = this.state.initCache.tryAcquireInitialization(userId);
|
|
60
|
+
|
|
61
|
+
if (!shouldInit && existingPromise) {
|
|
62
|
+
return existingPromise;
|
|
63
|
+
}
|
|
42
64
|
|
|
43
65
|
const promise = (async () => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
console.error('[SubscriptionManager] Service instance not available after initialization');
|
|
50
|
-
}
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
this.packageHandler!.setService(this.serviceInstance);
|
|
54
|
-
const result = await this.serviceInstance.initialize(effectiveUserId);
|
|
55
|
-
return result.success;
|
|
56
|
-
} catch (error) {
|
|
57
|
-
if (__DEV__) {
|
|
58
|
-
console.error('[SubscriptionManager] Initialization failed:', error);
|
|
59
|
-
}
|
|
60
|
-
return false;
|
|
66
|
+
await initializeRevenueCatService(this.managerConfig!.config);
|
|
67
|
+
this.serviceInstance = getRevenueCatService();
|
|
68
|
+
|
|
69
|
+
if (!this.serviceInstance) {
|
|
70
|
+
throw new Error("Service instance not available after initialization");
|
|
61
71
|
}
|
|
72
|
+
|
|
73
|
+
this.ensurePackageHandlerInitialized();
|
|
74
|
+
const result = await this.serviceInstance.initialize(userId);
|
|
75
|
+
return result.success;
|
|
62
76
|
})();
|
|
63
77
|
|
|
64
|
-
this.state.initCache.setPromise(promise,
|
|
78
|
+
this.state.initCache.setPromise(promise, userId);
|
|
65
79
|
return promise;
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
isInitializedForUser(userId: string): boolean {
|
|
69
|
-
|
|
83
|
+
if (!this.serviceInstance) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!this.serviceInstance.isInitialized()) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return this.state.initCache.getCurrentUserId() === userId;
|
|
70
92
|
}
|
|
71
93
|
|
|
72
94
|
async getPackages(): Promise<PurchasesPackage[]> {
|
|
73
95
|
this.ensureConfigured();
|
|
96
|
+
|
|
74
97
|
if (!this.serviceInstance) {
|
|
75
98
|
this.serviceInstance = getRevenueCatService();
|
|
76
|
-
this.packageHandler!.setService(this.serviceInstance);
|
|
77
99
|
}
|
|
100
|
+
|
|
101
|
+
if (!this.serviceInstance) {
|
|
102
|
+
throw new Error("Service instance not available");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.ensurePackageHandlerInitialized();
|
|
78
106
|
return this.packageHandler!.fetchPackages();
|
|
79
107
|
}
|
|
80
108
|
|
|
81
109
|
async purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
|
|
82
110
|
this.ensureConfigured();
|
|
111
|
+
|
|
83
112
|
const userId = this.state.initCache.getCurrentUserId();
|
|
84
|
-
if (!userId)
|
|
113
|
+
if (!userId) {
|
|
114
|
+
throw new Error("No current user found");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.ensurePackageHandlerInitialized();
|
|
85
118
|
return this.packageHandler!.purchase(pkg, userId);
|
|
86
119
|
}
|
|
87
120
|
|
|
88
121
|
async restore(): Promise<RestoreResultInfo> {
|
|
89
122
|
this.ensureConfigured();
|
|
123
|
+
|
|
90
124
|
const userId = this.state.initCache.getCurrentUserId();
|
|
91
|
-
if (!userId)
|
|
125
|
+
if (!userId) {
|
|
126
|
+
throw new Error("No current user found");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.ensurePackageHandlerInitialized();
|
|
92
130
|
return this.packageHandler!.restore(userId);
|
|
93
131
|
}
|
|
94
132
|
|
|
95
133
|
async checkPremiumStatus(): Promise<PremiumStatus> {
|
|
96
134
|
this.ensureConfigured();
|
|
135
|
+
|
|
97
136
|
const userId = this.state.initCache.getCurrentUserId();
|
|
98
|
-
if (!userId)
|
|
137
|
+
if (!userId) {
|
|
138
|
+
throw new Error("No current user found");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!this.serviceInstance) {
|
|
142
|
+
throw new Error("Service instance not available");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const customerInfo = await this.serviceInstance.getCustomerInfo();
|
|
99
146
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (customerInfo) return this.packageHandler!.checkPremiumStatusFromInfo(customerInfo);
|
|
103
|
-
} catch (error) {
|
|
104
|
-
throw error;
|
|
147
|
+
if (!customerInfo) {
|
|
148
|
+
throw new Error("Customer info not available");
|
|
105
149
|
}
|
|
106
|
-
|
|
150
|
+
|
|
151
|
+
this.ensurePackageHandlerInitialized();
|
|
152
|
+
return this.packageHandler!.checkPremiumStatusFromInfo(customerInfo);
|
|
107
153
|
}
|
|
108
154
|
|
|
109
155
|
async reset(): Promise<void> {
|
|
110
|
-
if (this.serviceInstance)
|
|
156
|
+
if (this.serviceInstance) {
|
|
157
|
+
await this.serviceInstance.reset();
|
|
158
|
+
}
|
|
159
|
+
|
|
111
160
|
this.state.reset();
|
|
112
161
|
this.serviceInstance = null;
|
|
113
162
|
}
|
|
114
163
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
164
|
+
isConfigured(): boolean {
|
|
165
|
+
return this.managerConfig !== null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
isInitialized(): boolean {
|
|
169
|
+
if (!this.serviceInstance) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return this.serviceInstance.isInitialized();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getEntitlementId(): string {
|
|
177
|
+
if (!this.managerConfig) {
|
|
178
|
+
throw new Error("SubscriptionManager not configured");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return this.managerConfig.config.entitlementIdentifier;
|
|
182
|
+
}
|
|
119
183
|
}
|
|
120
184
|
|
|
121
185
|
export const SubscriptionManager = new SubscriptionManagerImpl();
|
|
@@ -15,14 +15,16 @@ export async function handleRestore(deps: RestoreHandlerDeps, userId: string): P
|
|
|
15
15
|
|
|
16
16
|
try {
|
|
17
17
|
const customerInfo = await Purchases.restorePurchases();
|
|
18
|
-
const
|
|
18
|
+
const entitlement = customerInfo.entitlements.active[deps.config.entitlementIdentifier];
|
|
19
|
+
const isPremium = !!entitlement;
|
|
20
|
+
const productId = entitlement?.productIdentifier ?? null;
|
|
19
21
|
|
|
20
22
|
if (isPremium) {
|
|
21
23
|
await syncPremiumStatus(deps.config, userId, customerInfo);
|
|
22
24
|
}
|
|
23
25
|
await notifyRestoreCompleted(deps.config, userId, isPremium, customerInfo);
|
|
24
26
|
|
|
25
|
-
return { success: true, isPremium, customerInfo };
|
|
27
|
+
return { success: true, isPremium, productId, customerInfo };
|
|
26
28
|
} catch (error) {
|
|
27
29
|
throw new RevenueCatRestoreError(getErrorMessage(error, "Restore failed"));
|
|
28
30
|
}
|
|
@@ -19,8 +19,7 @@ const configurationState = {
|
|
|
19
19
|
configurationPromise: null as Promise<ReturnType<typeof initializeSDK>> | null,
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
// Simple lock mechanism to prevent concurrent configurations
|
|
23
|
-
let configurationLocks = new Set<string>();
|
|
22
|
+
// Simple lock mechanism to prevent concurrent configurations (implementation deferred)
|
|
24
23
|
|
|
25
24
|
function configureLogHandler(): void {
|
|
26
25
|
if (configurationState.isLogHandlerConfigured) return;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { CustomerInfo } from "react-native-purchases";
|
|
8
|
-
import { detectPackageType
|
|
8
|
+
import { detectPackageType } from "../../../../utils/packageTypeDetector";
|
|
9
9
|
|
|
10
10
|
export interface RenewalState {
|
|
11
11
|
previousExpirationDate: string | null;
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
import React, { useMemo } from "react";
|
|
7
7
|
import { View, StyleSheet } from "react-native";
|
|
8
8
|
import { useAppDesignTokens, AtomicText } from "@umituz/react-native-design-system";
|
|
9
|
-
import {
|
|
10
|
-
SUBSCRIPTION_STATUS,
|
|
11
|
-
type SubscriptionStatusType
|
|
12
|
-
} from "../../../
|
|
9
|
+
import {
|
|
10
|
+
SUBSCRIPTION_STATUS,
|
|
11
|
+
type SubscriptionStatusType
|
|
12
|
+
} from "../../../core/SubscriptionConstants";
|
|
13
|
+
|
|
14
|
+
export type { SubscriptionStatusType };
|
|
13
15
|
|
|
14
16
|
export interface PremiumStatusBadgeProps {
|
|
15
17
|
status: SubscriptionStatusType;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useMemo } from "react";
|
|
2
2
|
import { View, TouchableOpacity, TextInput } from "react-native";
|
|
3
3
|
import { AtomicText, BaseModal, useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
4
|
-
import { usePaywallFeedback } from "
|
|
4
|
+
import { usePaywallFeedback } from "../../../../../presentation/hooks/feedback/usePaywallFeedback";
|
|
5
5
|
import { createPaywallFeedbackStyles } from "./paywallFeedbackStyles";
|
|
6
6
|
|
|
7
7
|
const FEEDBACK_OPTION_IDS = [
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Type definitions for subscription detail screen and components
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { SubscriptionStatusType } from "../../
|
|
6
|
+
import type { SubscriptionStatusType } from "../../core/SubscriptionStatus";
|
|
7
7
|
import type { CreditInfo } from "../components/details/PremiumDetailsCardTypes";
|
|
8
8
|
|
|
9
9
|
export type { SubscriptionStatusType, CreditInfo };
|
|
@@ -16,6 +16,8 @@ export interface SubscriptionDetailTranslations {
|
|
|
16
16
|
statusExpired: string;
|
|
17
17
|
statusInactive: string;
|
|
18
18
|
statusCanceled: string;
|
|
19
|
+
/** Free status label */
|
|
20
|
+
statusFree: string;
|
|
19
21
|
/** Trial status label (defaults to statusActive if not provided) */
|
|
20
22
|
statusTrial?: string;
|
|
21
23
|
/** Trial canceled status label (defaults to statusCanceled if not provided) */
|
|
@@ -109,7 +111,7 @@ export interface SubscriptionHeaderProps {
|
|
|
109
111
|
| "expiresLabel"
|
|
110
112
|
| "purchasedLabel"
|
|
111
113
|
| "lifetimeLabel"
|
|
112
|
-
|
|
114
|
+
> & { statusFree: string };
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
/** Props for credits list component */
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Type definitions for subscription settings configuration
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { SubscriptionStatusType } from "../../
|
|
6
|
+
import type { SubscriptionStatusType } from "../../core/SubscriptionConstants";
|
|
7
7
|
import type {
|
|
8
8
|
SubscriptionDetailConfig,
|
|
9
9
|
UpgradePromptConfig,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { getSavedPurchase, clearSavedPurchase } from "./useAuthAwarePurchase";
|
|
13
13
|
import { usePremium } from "./usePremium";
|
|
14
14
|
import { SubscriptionManager } from "../infrastructure/managers/SubscriptionManager";
|
|
15
|
-
import { usePurchaseLoadingStore } from "
|
|
15
|
+
import { usePurchaseLoadingStore } from "./stores";
|
|
16
16
|
|
|
17
17
|
export interface UseSavedPurchaseAutoExecutionParams {
|
|
18
18
|
onSuccess?: () => void;
|
|
@@ -7,21 +7,21 @@ import { useMemo, useCallback } from "react";
|
|
|
7
7
|
import { useCredits } from "../../credits/presentation/useCredits";
|
|
8
8
|
import { usePaywallVisibility } from "./usePaywallVisibility";
|
|
9
9
|
import { calculateDaysRemaining } from "../core/SubscriptionStatus";
|
|
10
|
-
import { formatDate } from "
|
|
10
|
+
import { formatDate } from "./utils/subscriptionDateUtils";
|
|
11
11
|
import { useCreditsArray, getSubscriptionStatusType } from "./useSubscriptionSettingsConfig.utils";
|
|
12
12
|
import { getCreditsConfig } from "../../credits/infrastructure/CreditsRepositoryProvider";
|
|
13
13
|
import type {
|
|
14
14
|
SubscriptionSettingsConfig,
|
|
15
15
|
SubscriptionStatusType,
|
|
16
16
|
UseSubscriptionSettingsConfigParams,
|
|
17
|
-
} from "
|
|
17
|
+
} from "./types/SubscriptionSettingsTypes";
|
|
18
18
|
|
|
19
19
|
export type {
|
|
20
20
|
SubscriptionSettingsConfig,
|
|
21
21
|
SubscriptionSettingsItemConfig,
|
|
22
22
|
SubscriptionSettingsTranslations,
|
|
23
23
|
UseSubscriptionSettingsConfigParams,
|
|
24
|
-
} from "
|
|
24
|
+
} from "./types/SubscriptionSettingsTypes";
|
|
25
25
|
|
|
26
26
|
export const useSubscriptionSettingsConfig = (
|
|
27
27
|
params: Omit<UseSubscriptionSettingsConfigParams, 'userId'>
|
|
@@ -95,6 +95,7 @@ export const useSubscriptionSettingsConfig = (
|
|
|
95
95
|
statusExpired: translations.statusExpired,
|
|
96
96
|
statusInactive: translations.statusInactive,
|
|
97
97
|
statusCanceled: translations.statusCanceled,
|
|
98
|
+
statusFree: translations.statusInactive,
|
|
98
99
|
expiresLabel: translations.expiresLabel,
|
|
99
100
|
purchasedLabel: translations.purchasedLabel,
|
|
100
101
|
lifetimeLabel: translations.lifetimeLabel,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { useMemo } from "react";
|
|
7
7
|
import type { UserCredits } from "../../credits/core/Credits";
|
|
8
8
|
import { resolveSubscriptionStatus, type PeriodType, type SubscriptionStatusType } from "../core/SubscriptionStatus";
|
|
9
|
-
import type { SubscriptionSettingsTranslations } from "
|
|
9
|
+
import type { SubscriptionSettingsTranslations } from "./types/SubscriptionSettingsTypes";
|
|
10
10
|
|
|
11
11
|
export interface CreditsInfo {
|
|
12
12
|
id: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { doc, getDoc, setDoc, serverTimestamp,
|
|
1
|
+
import { doc, getDoc, setDoc, serverTimestamp, type Firestore } from "firebase/firestore";
|
|
2
2
|
import { getFirestore } from "@umituz/react-native-firebase";
|
|
3
|
-
import type { DeviceTrialRecord } from "
|
|
3
|
+
import type { DeviceTrialRecord } from "../core/TrialTypes";
|
|
4
4
|
|
|
5
5
|
const DEVICE_TRIALS_COLLECTION = "device_trials";
|
|
6
6
|
|
|
@@ -22,10 +22,13 @@ export class SubscriptionEventBus {
|
|
|
22
22
|
this.listeners[event] = [];
|
|
23
23
|
}
|
|
24
24
|
this.listeners[event].push(callback);
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
// Return unsubscribe function
|
|
27
27
|
return () => {
|
|
28
|
-
|
|
28
|
+
const listeners = this.listeners[event];
|
|
29
|
+
if (listeners) {
|
|
30
|
+
this.listeners[event] = listeners.filter(l => l !== callback);
|
|
31
|
+
}
|
|
29
32
|
};
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
# Presentation Layer
|
|
2
|
-
|
|
3
|
-
UI/UX layer for the subscription system - React hooks, components, and screens.
|
|
4
|
-
|
|
5
|
-
## Location
|
|
6
|
-
|
|
7
|
-
**Directory**: `src/presentation/`
|
|
8
|
-
|
|
9
|
-
**Type**: Layer
|
|
10
|
-
|
|
11
|
-
## Strategy
|
|
12
|
-
|
|
13
|
-
### Layer Responsibilities
|
|
14
|
-
|
|
15
|
-
The Presentation Layer is responsible for:
|
|
16
|
-
|
|
17
|
-
1. **State Management**
|
|
18
|
-
- React hooks for data fetching and mutations
|
|
19
|
-
- TanStack Query for server state management
|
|
20
|
-
- Local state management for UI state
|
|
21
|
-
|
|
22
|
-
2. **UI Components**
|
|
23
|
-
- Reusable subscription components
|
|
24
|
-
- Feature gating UI elements
|
|
25
|
-
- Credit display components
|
|
26
|
-
- Paywall components
|
|
27
|
-
|
|
28
|
-
3. **User Interaction**
|
|
29
|
-
- Handle user actions
|
|
30
|
-
- Display appropriate feedback
|
|
31
|
-
- Guide users through purchase flows
|
|
32
|
-
- Show upgrade prompts at right time
|
|
33
|
-
|
|
34
|
-
### Architecture Pattern
|
|
35
|
-
|
|
36
|
-
The presentation layer follows a layered architecture where:
|
|
37
|
-
- Hooks manage state and data fetching at the top level
|
|
38
|
-
- Components consume hooks and render UI
|
|
39
|
-
- Screens compose multiple components together
|
|
40
|
-
- All layers communicate with the domain layer for business logic
|
|
41
|
-
|
|
42
|
-
### Integration Points
|
|
43
|
-
|
|
44
|
-
- **Domain Layer**: Business logic and data access
|
|
45
|
-
- **TanStack Query**: Server state management
|
|
46
|
-
- **RevenueCat**: Purchase operations
|
|
47
|
-
- **Navigation**: Screen routing
|
|
48
|
-
|
|
49
|
-
## Restrictions
|
|
50
|
-
|
|
51
|
-
### REQUIRED
|
|
52
|
-
|
|
53
|
-
- **Type Safety**: All components MUST be typed with TypeScript
|
|
54
|
-
- **Error Boundaries**: MUST implement error boundaries for all screens
|
|
55
|
-
- **Loading States**: MUST show loading indicators during async operations
|
|
56
|
-
- **User Feedback**: MUST provide feedback for all user actions
|
|
57
|
-
|
|
58
|
-
### PROHIBITED
|
|
59
|
-
|
|
60
|
-
- **NEVER** include business logic in components (use hooks instead)
|
|
61
|
-
- **NEVER** make direct API calls from components (use hooks)
|
|
62
|
-
- **DO NOT** store sensitive data in component state
|
|
63
|
-
- **NEVER** hardcode strings (use localization)
|
|
64
|
-
|
|
65
|
-
### CRITICAL SAFETY
|
|
66
|
-
|
|
67
|
-
- **ALWAYS** validate props before rendering
|
|
68
|
-
- **ALWAYS** handle loading and error states
|
|
69
|
-
- **NEVER** trust client-side state for security decisions
|
|
70
|
-
- **MUST** implement proper error boundaries
|
|
71
|
-
- **ALWAYS** sanitize user inputs before display
|
|
72
|
-
|
|
73
|
-
## AI Agent Guidelines
|
|
74
|
-
|
|
75
|
-
### When Building Presentation Layer
|
|
76
|
-
|
|
77
|
-
1. **Always** use hooks for data fetching and state management
|
|
78
|
-
2. **Always** handle loading and error states
|
|
79
|
-
3. **Always** provide user feedback for actions
|
|
80
|
-
4. **Always** implement error boundaries
|
|
81
|
-
5. **Never** include business logic in components
|
|
82
|
-
|
|
83
|
-
### Integration Checklist
|
|
84
|
-
|
|
85
|
-
- [ ] Use appropriate hooks for data access
|
|
86
|
-
- [ ] Handle loading states
|
|
87
|
-
- [ ] Handle error states
|
|
88
|
-
- [ ] Implement error boundaries
|
|
89
|
-
- [ ] Provide user feedback
|
|
90
|
-
- [ ] Test with various data states
|
|
91
|
-
- [ ] Test error scenarios
|
|
92
|
-
- [ ] Ensure type safety
|
|
93
|
-
- [ ] Use localization for all strings
|
|
94
|
-
- [ ] Test accessibility
|
|
95
|
-
|
|
96
|
-
### Common Patterns
|
|
97
|
-
|
|
98
|
-
1. **Compound Components**: Build complex UIs from simple components
|
|
99
|
-
2. **Render Props**: Share stateful logic between components
|
|
100
|
-
3. **Custom Hooks**: Extract reusable stateful logic
|
|
101
|
-
4. **Error Boundaries**: Prevent crashes from propagating
|
|
102
|
-
5. **Loading Skeletons**: Show placeholder during loading
|
|
103
|
-
6. **Optimistic Updates**: Update UI immediately, rollback on failure
|
|
104
|
-
7. **Graceful Degradation**: Show limited version on error
|
|
105
|
-
8. **Responsive Design**: Support different screen sizes
|
|
106
|
-
|
|
107
|
-
## Related Documentation
|
|
108
|
-
|
|
109
|
-
- **Hooks**: `hooks/README.md`
|
|
110
|
-
- **Components**: `components/README.md`
|
|
111
|
-
- **Screens**: `screens/README.md`
|
|
112
|
-
- **Wallet Domain**: `../../domains/wallet/README.md`
|
|
113
|
-
- **Paywall Domain**: `../../domains/paywall/README.md`
|
|
114
|
-
- **RevenueCat**: `../../revenuecat/README.md`
|
|
115
|
-
|
|
116
|
-
## Directory Structure
|
|
117
|
-
|
|
118
|
-
The presentation layer contains:
|
|
119
|
-
- **hooks/** - React hooks for state management (usePremium, useSubscription, useCredits, useDeductCredit, useFeatureGate)
|
|
120
|
-
- **components/** - UI components organized by functionality
|
|
121
|
-
- **details/** - Detail cards, badges
|
|
122
|
-
- **feedback/** - Modals, feedback components
|
|
123
|
-
- **sections/** - Section components
|
|
124
|
-
- **paywall/** - Paywall components
|
|
125
|
-
- **screens/** - Full-screen components (SubscriptionDetailScreen)
|