@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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/application/CreditsInitializer.ts +91 -38
  3. package/src/domains/credits/core/UserCreditsDocument.ts +33 -33
  4. package/src/domains/credits/infrastructure/CreditsRepository.ts +44 -58
  5. package/src/domains/paywall/components/PaywallModal.tsx +1 -1
  6. package/src/domains/subscription/application/SubscriptionInitializer.ts +59 -18
  7. package/src/domains/subscription/infrastructure/handlers/PackageHandler.ts +46 -27
  8. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +106 -42
  9. package/src/domains/subscription/infrastructure/services/RestoreHandler.ts +4 -2
  10. package/src/domains/subscription/infrastructure/services/RevenueCatInitializer.ts +1 -2
  11. package/src/domains/subscription/infrastructure/utils/RenewalDetector.ts +1 -1
  12. package/src/domains/subscription/presentation/components/details/PremiumStatusBadge.tsx +6 -4
  13. package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackModal.tsx +1 -1
  14. package/src/domains/subscription/presentation/types/SubscriptionDetailTypes.ts +4 -2
  15. package/src/domains/subscription/presentation/types/SubscriptionSettingsTypes.ts +1 -1
  16. package/src/domains/subscription/presentation/usePremiumGate.ts +1 -1
  17. package/src/domains/subscription/presentation/useSavedPurchaseAutoExecution.ts +1 -1
  18. package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.ts +4 -3
  19. package/src/domains/subscription/presentation/useSubscriptionSettingsConfig.utils.ts +1 -1
  20. package/src/domains/trial/application/TrialEligibilityService.ts +1 -1
  21. package/src/domains/trial/infrastructure/DeviceTrialRepository.ts +2 -2
  22. package/src/shared/application/ports/IRevenueCatService.ts +2 -0
  23. package/src/shared/infrastructure/SubscriptionEventBus.ts +5 -2
  24. package/src/presentation/README.md +0 -125
  25. package/src/presentation/hooks/README.md +0 -156
  26. package/src/presentation/hooks/useAuthSubscriptionSync.md +0 -94
  27. package/src/presentation/hooks/useCredits.md +0 -103
  28. package/src/presentation/hooks/useDeductCredit.md +0 -100
  29. package/src/presentation/hooks/useFeatureGate.md +0 -112
  30. package/src/presentation/hooks/usePaywall.md +0 -89
  31. package/src/presentation/hooks/usePaywallOperations.md +0 -92
  32. package/src/presentation/hooks/usePaywallVisibility.md +0 -95
  33. package/src/presentation/hooks/usePremium.md +0 -88
  34. 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?: () => Promise<string>;
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.packageHandler = new PackageHandler(null, config.config.entitlementIdentifier);
29
- if (config.getAnonymousUserId) this.state.userIdProvider.configure(config.getAnonymousUserId);
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 || !this.packageHandler) throw new Error("SubscriptionManager not configured");
51
+ if (!this.managerConfig) {
52
+ throw new Error("SubscriptionManager not configured");
53
+ }
34
54
  }
35
55
 
36
- async initialize(userId?: string): Promise<boolean> {
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
- if (!shouldInit && existingPromise) return existingPromise;
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
- try {
45
- await initializeRevenueCatService(this.managerConfig!.config);
46
- this.serviceInstance = getRevenueCatService();
47
- if (!this.serviceInstance) {
48
- if (__DEV__) {
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, effectiveUserId);
78
+ this.state.initCache.setPromise(promise, userId);
65
79
  return promise;
66
80
  }
67
81
 
68
82
  isInitializedForUser(userId: string): boolean {
69
- return this.serviceInstance?.isInitialized() === true && this.state.initCache.getCurrentUserId() === userId;
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) return false;
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) return { success: false, productId: null };
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) return { isPremium: false, expirationDate: null };
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
- try {
101
- const customerInfo = await this.serviceInstance?.getCustomerInfo();
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
- return { isPremium: false, expirationDate: null };
150
+
151
+ this.ensurePackageHandlerInitialized();
152
+ return this.packageHandler!.checkPremiumStatusFromInfo(customerInfo);
107
153
  }
108
154
 
109
155
  async reset(): Promise<void> {
110
- if (this.serviceInstance) await this.serviceInstance.reset();
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
- // Helper status checks
116
- isConfigured = () => !!this.managerConfig;
117
- isInitialized = () => this.serviceInstance?.isInitialized() ?? false;
118
- getEntitlementId = () => this.managerConfig?.config.entitlementIdentifier || null;
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 isPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
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, type SubscriptionPackageType } from "../../../../utils/packageTypeDetector";
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 "../../../domains/subscription/core/SubscriptionConstants";
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 "../../hooks/feedback/usePaywallFeedback";
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 "../../domains/subscription/core/SubscriptionStatus";
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 "../../domains/subscription/core/SubscriptionConstants";
6
+ import type { SubscriptionStatusType } from "../../core/SubscriptionConstants";
7
7
  import type {
8
8
  SubscriptionDetailConfig,
9
9
  UpgradePromptConfig,
@@ -27,7 +27,7 @@
27
27
  * ```
28
28
  */
29
29
 
30
- import { useCallback, useEffect } from "react";
30
+ import { useCallback } from "react";
31
31
  import { useSubscriptionStatus } from "./useSubscriptionStatus";
32
32
  import { paywallControl } from "./usePaywallVisibility";
33
33
 
@@ -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 "../../../presentation/stores";
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 "../../../presentation/utils/subscriptionDateUtils";
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 "../../../presentation/types/SubscriptionSettingsTypes";
17
+ } from "./types/SubscriptionSettingsTypes";
18
18
 
19
19
  export type {
20
20
  SubscriptionSettingsConfig,
21
21
  SubscriptionSettingsItemConfig,
22
22
  SubscriptionSettingsTranslations,
23
23
  UseSubscriptionSettingsConfigParams,
24
- } from "../../../presentation/types/SubscriptionSettingsTypes";
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 "../types/SubscriptionSettingsTypes";
9
+ import type { SubscriptionSettingsTranslations } from "./types/SubscriptionSettingsTypes";
10
10
 
11
11
  export interface CreditsInfo {
12
12
  id: string;
@@ -1,4 +1,4 @@
1
- import type { TrialEligibilityResult, DeviceTrialRecord } from "./TrialTypes";
1
+ import type { TrialEligibilityResult, DeviceTrialRecord } from "../core/TrialTypes";
2
2
 
3
3
  export class TrialEligibilityService {
4
4
  static check(
@@ -1,6 +1,6 @@
1
- import { doc, getDoc, setDoc, serverTimestamp, arrayUnion, type Firestore } from "firebase/firestore";
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 "./TrialTypes";
3
+ import type { DeviceTrialRecord } from "../core/TrialTypes";
4
4
 
5
5
  const DEVICE_TRIALS_COLLECTION = "device_trials";
6
6
 
@@ -21,6 +21,8 @@ export interface PurchaseResult {
21
21
  export interface RestoreResult {
22
22
  success: boolean;
23
23
  productId: string | null;
24
+ isPremium?: boolean;
25
+ customerInfo?: CustomerInfo;
24
26
  }
25
27
 
26
28
  export interface IRevenueCatService {
@@ -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
- this.listeners[event] = this.listeners[event].filter(l => l !== callback);
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)