@umituz/react-native-subscription 2.14.98 → 2.14.99

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.
@@ -0,0 +1,350 @@
1
+ # Config Domain Entities
2
+
3
+ Domain entities for configuration management.
4
+
5
+ ## Overview
6
+
7
+ This directory contains entity classes representing configuration concepts like packages, features, and paywalls.
8
+
9
+ ## Entities
10
+
11
+ ### PackageConfig
12
+
13
+ Represents a subscription package configuration.
14
+
15
+ **File**: `PackageConfig.ts`
16
+
17
+ ```typescript
18
+ class PackageConfig {
19
+ readonly identifier: string;
20
+ readonly productId: string;
21
+ readonly period: PackagePeriod;
22
+ readonly price: Money;
23
+ readonly features: string[];
24
+ readonly credits?: number;
25
+ readonly metadata: PackageMetadata;
26
+
27
+ // Methods
28
+ isAnnual(): boolean;
29
+ isMonthly(): boolean;
30
+ isLifetime(): boolean;
31
+ hasCredits(): boolean;
32
+ isRecommended(): boolean;
33
+ isHighlighted(): boolean;
34
+ getPerMonthPrice(): Money | null;
35
+ getDiscountPercentage(): number | null;
36
+ getBadge(): string | null;
37
+ }
38
+ ```
39
+
40
+ **Usage:**
41
+ ```typescript
42
+ const pkg = new PackageConfig({
43
+ identifier: 'premium_annual',
44
+ productId: 'com.app.premium.annual',
45
+ period: 'annual',
46
+ price: 79.99,
47
+ currency: 'USD',
48
+ features: ['Unlimited Access', 'Ad-Free'],
49
+ credits: 1200,
50
+ metadata: {
51
+ recommended: true,
52
+ badge: 'Best Value',
53
+ discount: { percentage: 20, description: 'Save 20%' },
54
+ },
55
+ });
56
+
57
+ console.log(pkg.isAnnual()); // true
58
+ console.log(pkg.getPerMonthPrice()?.format()); // '$6.67'
59
+ console.log(pkg.getDiscountPercentage()); // 20
60
+ ```
61
+
62
+ ### FeatureConfig
63
+
64
+ Represents a feature configuration with gating rules.
65
+
66
+ **File**: `FeatureConfig.ts`
67
+
68
+ ```typescript
69
+ class FeatureConfig {
70
+ readonly id: string;
71
+ readonly name: string;
72
+ readonly description?: string;
73
+ readonly requiresPremium: boolean;
74
+ readonly requiresCredits: boolean;
75
+ readonly creditCost?: number;
76
+ readonly enabled: boolean;
77
+ readonly gateType: 'premium' | 'credits' | 'both';
78
+
79
+ // Methods
80
+ isAccessible(userHasPremium: boolean, userCredits: number): boolean;
81
+ getRequiredCredits(): number;
82
+ }
83
+ ```
84
+
85
+ **Usage:**
86
+ ```typescript
87
+ const feature = new FeatureConfig({
88
+ id: 'ai_generation',
89
+ name: 'AI Generation',
90
+ requiresPremium: false,
91
+ requiresCredits: true,
92
+ creditCost: 5,
93
+ enabled: true,
94
+ gateType: 'credits',
95
+ });
96
+
97
+ console.log(feature.isAccessible(false, 10)); // true
98
+ console.log(feature.isAccessible(false, 3)); // false
99
+ console.log(feature.getRequiredCredits()); // 5
100
+ ```
101
+
102
+ ### PaywallConfig
103
+
104
+ Represents a paywall screen configuration.
105
+
106
+ **File**: `PaywallConfig.ts`
107
+
108
+ ```typescript
109
+ class PaywallConfig {
110
+ readonly id: string;
111
+ readonly title: string;
112
+ readonly subtitle?: string;
113
+ readonly features: string[];
114
+ readonly packages: PackageConfig[];
115
+ readonly highlightPackage?: string;
116
+ readonly style: PaywallStyle;
117
+ readonly triggers: PaywallTrigger[];
118
+
119
+ // Methods
120
+ getHighlightedPackage(): PackageConfig | null;
121
+ getPackageByIdentifier(identifier: string): PackageConfig | null;
122
+ shouldTrigger(triggerType: string): boolean;
123
+ }
124
+ ```
125
+
126
+ **Usage:**
127
+ ```typescript
128
+ const paywall = new PaywallConfig({
129
+ id: 'premium_upgrade',
130
+ title: 'Upgrade to Premium',
131
+ features: ['Unlimited Access', 'Ad-Free'],
132
+ packages: [monthlyPkg, annualPkg],
133
+ highlightPackage: 'premium_annual',
134
+ style: {
135
+ primaryColor: '#007AFF',
136
+ backgroundColor: '#FFFFFF',
137
+ },
138
+ triggers: [{ type: 'credit_gate', enabled: true }],
139
+ });
140
+
141
+ const highlighted = paywall.getHighlightedPackage();
142
+ console.log(highlighted?.identifier); // 'premium_annual'
143
+ ```
144
+
145
+ ### SubscriptionSettingsConfig
146
+
147
+ Represents subscription settings configuration.
148
+
149
+ **File**: `SubscriptionSettingsConfig.ts`
150
+
151
+ ```typescript
152
+ class SubscriptionSettingsConfig {
153
+ readonly showSubscriptionDetails: boolean;
154
+ readonly showCreditBalance: boolean;
155
+ readonly allowRestorePurchases: boolean;
156
+ readonly showManageSubscriptionButton: boolean;
157
+ readonly subscriptionManagementURL: string;
158
+ readonly supportEmail: string;
159
+
160
+ // Methods
161
+ getAvailableActions(): string[];
162
+ isRestoreAllowed(): boolean;
163
+ }
164
+ ```
165
+
166
+ **Usage:**
167
+ ```typescript
168
+ const settings = new SubscriptionSettingsConfig({
169
+ showSubscriptionDetails: true,
170
+ showCreditBalance: true,
171
+ allowRestorePurchases: true,
172
+ showManageSubscriptionButton: true,
173
+ subscriptionManagementURL: 'https://apps.apple.com/account/subscriptions',
174
+ supportEmail: 'support@example.com',
175
+ });
176
+
177
+ console.log(settings.isRestoreAllowed()); // true
178
+ ```
179
+
180
+ ## Supporting Types
181
+
182
+ ### PackageMetadata
183
+
184
+ ```typescript
185
+ interface PackageMetadata {
186
+ highlight?: boolean;
187
+ recommended?: boolean;
188
+ discount?: {
189
+ percentage: number;
190
+ description: string;
191
+ };
192
+ badge?: string;
193
+ }
194
+ ```
195
+
196
+ ### PaywallStyle
197
+
198
+ ```typescript
199
+ interface PaywallStyle {
200
+ primaryColor: string;
201
+ backgroundColor: string;
202
+ textColor?: string;
203
+ image?: string;
204
+ logo?: string;
205
+ }
206
+ ```
207
+
208
+ ### PaywallTrigger
209
+
210
+ ```typescript
211
+ interface PaywallTrigger {
212
+ type: string;
213
+ enabled: boolean;
214
+ conditions?: Record<string, unknown>;
215
+ }
216
+ ```
217
+
218
+ ## Factory Functions
219
+
220
+ ### createDefaultPackages
221
+
222
+ Create default package configurations.
223
+
224
+ ```typescript
225
+ function createDefaultPackages(): PackageConfig[] {
226
+ return [
227
+ new PackageConfig({
228
+ identifier: 'premium_monthly',
229
+ productId: 'com.app.premium.monthly',
230
+ period: 'monthly',
231
+ price: 9.99,
232
+ currency: 'USD',
233
+ features: ['Unlimited Access', 'Ad-Free'],
234
+ credits: 100,
235
+ }),
236
+ new PackageConfig({
237
+ identifier: 'premium_annual',
238
+ productId: 'com.app.premium.annual',
239
+ period: 'annual',
240
+ price: 79.99,
241
+ currency: 'USD',
242
+ features: ['Unlimited Access', 'Ad-Free', 'Save 20%'],
243
+ credits: 1200,
244
+ metadata: {
245
+ recommended: true,
246
+ badge: 'Best Value',
247
+ discount: { percentage: 20, description: 'Save 20%' },
248
+ },
249
+ }),
250
+ ];
251
+ }
252
+ ```
253
+
254
+ ### createDefaultPaywall
255
+
256
+ Create default paywall configuration.
257
+
258
+ ```typescript
259
+ function createDefaultPaywall(): PaywallConfig {
260
+ return new PaywallConfig({
261
+ id: 'default_paywall',
262
+ title: 'Upgrade to Premium',
263
+ subtitle: 'Get unlimited access to all features',
264
+ features: [
265
+ 'Unlimited Access',
266
+ 'Ad-Free Experience',
267
+ 'Priority Support',
268
+ 'Exclusive Features',
269
+ ],
270
+ packages: createDefaultPackages(),
271
+ highlightPackage: 'premium_annual',
272
+ style: {
273
+ primaryColor: '#007AFF',
274
+ backgroundColor: '#FFFFFF',
275
+ },
276
+ triggers: [
277
+ { type: 'premium_feature', enabled: true },
278
+ { type: 'credit_gate', enabled: true },
279
+ ],
280
+ });
281
+ }
282
+ ```
283
+
284
+ ## Usage Examples
285
+
286
+ ### Validating Package Configuration
287
+
288
+ ```typescript
289
+ function validatePackage(config: PackageConfigData): boolean {
290
+ try {
291
+ new PackageConfig(config);
292
+ return true;
293
+ } catch (error) {
294
+ console.error('Invalid package config:', error.message);
295
+ return false;
296
+ }
297
+ }
298
+ ```
299
+
300
+ ### Finding Recommended Package
301
+
302
+ ```typescript
303
+ function findRecommendedPackage(packages: PackageConfig[]): PackageConfig | null {
304
+ return packages.find(pkg => pkg.isRecommended()) ?? null;
305
+ }
306
+
307
+ const recommended = findRecommendedPackage(packages);
308
+ console.log('Recommended:', recommended?.identifier);
309
+ ```
310
+
311
+ ### Filtering Packages by Period
312
+
313
+ ```typescript
314
+ function getPackagesByPeriod(packages: PackageConfig[], period: 'monthly' | 'annual'): PackageConfig[] {
315
+ return packages.filter(pkg => {
316
+ if (period === 'monthly') return pkg.isMonthly();
317
+ if (period === 'annual') return pkg.isAnnual();
318
+ return false;
319
+ });
320
+ }
321
+
322
+ const monthlyPackages = getPackagesByPeriod(packages, 'monthly');
323
+ ```
324
+
325
+ ### Checking Feature Access
326
+
327
+ ```typescript
328
+ function canUserAccessFeature(feature: FeatureConfig, user: User): boolean {
329
+ return feature.isAccessible(user.isPremium, user.credits);
330
+ }
331
+
332
+ const aiFeature = new FeatureConfig({ /* ... */ });
333
+ const canAccess = canUserAccessFeature(aiFeature, currentUser);
334
+ ```
335
+
336
+ ## Best Practices
337
+
338
+ 1. **Validation**: Always validate configuration in constructor
339
+ 2. **Immutability**: Never modify entities after creation
340
+ 3. **Business Logic**: Keep business logic in entities
341
+ 4. **Type Safety**: Use TypeScript strictly
342
+ 5. **Error Messages**: Provide clear error messages
343
+ 6. **Defaults**: Provide factory functions for defaults
344
+ 7. **Testing**: Test validation logic thoroughly
345
+
346
+ ## Related
347
+
348
+ - [Config Domain](../README.md)
349
+ - [Config Value Objects](../value-objects/README.md)
350
+ - [Config Utils](../../utils/README.md)
@@ -2,159 +2,175 @@
2
2
 
3
3
  Hook for deducting credits from user balance with optimistic updates.
4
4
 
5
- ## Import
5
+ ## Location
6
6
 
7
- ```typescript
8
- import { useDeductCredit } from '@umituz/react-native-subscription';
9
- ```
7
+ **Import Path**: `@umituz/react-native-subscription`
10
8
 
11
- ## Signature
9
+ **File**: `src/presentation/hooks/useDeductCredit.ts`
12
10
 
13
- ```typescript
14
- function useDeductCredit(params: {
15
- userId: string | undefined;
16
- onCreditsExhausted?: () => void;
17
- }): {
18
- deductCredit: (cost?: number) => Promise<boolean>;
19
- deductCredits: (cost: number) => Promise<boolean>;
20
- isDeducting: boolean;
21
- }
22
- ```
11
+ ## Strategy
12
+
13
+ ### Credit Deduction Flow
14
+
15
+ 1. **Pre-deduction Validation**
16
+ - Verify user is authenticated (userId must be defined)
17
+ - Check if sufficient credits exist
18
+ - Validate deduction amount is positive
19
+
20
+ 2. **Optimistic Update**
21
+ - Immediately update UI with new balance
22
+ - Store previous state for potential rollback
23
+ - Update TanStack Query cache
24
+
25
+ 3. **Server Synchronization**
26
+ - Send deduction request to backend
27
+ - Handle success/failure responses
28
+ - Rollback on failure
29
+
30
+ 4. **Post-deduction Handling**
31
+ - Trigger `onCreditsExhausted` callback if balance reaches zero
32
+ - Return success/failure boolean
33
+ - Reset loading state
34
+
35
+ ### Integration Points
36
+
37
+ - **Credits Repository**: `src/domains/wallet/infrastructure/repositories/CreditsRepository.ts`
38
+ - **Credits Entity**: `src/domains/wallet/domain/entities/UserCredits.ts`
39
+ - **TanStack Query**: For cache management and optimistic updates
40
+
41
+ ## Restrictions
42
+
43
+ ### REQUIRED
23
44
 
24
- ## Parameters
45
+ - **User Authentication**: `userId` parameter MUST be provided and cannot be undefined
46
+ - **Positive Amount**: Credit cost MUST be greater than zero
47
+ - **Callback Implementation**: `onCreditsExhausted` callback SHOULD be implemented to handle zero balance scenarios
25
48
 
26
- | Parameter | Type | Default | Description |
27
- |-----------|------|---------|-------------|
28
- | `userId` | `string \| undefined` | **Required** | User ID for credit deduction |
29
- | `onCreditsExhausted` | `() => void` | `undefined` | Callback when credits are exhausted |
49
+ ### PROHIBITED
30
50
 
31
- ## Returns
51
+ - **NEVER** call `deductCredit` or `deductCredits` without checking `isDeducting` state first
52
+ - **NEVER** allow multiple simultaneous deduction calls for the same user
53
+ - **NEVER** deduce credits when balance is insufficient (should check with `useCreditChecker` first)
54
+ - **DO NOT** use this hook for guest users (userId undefined)
32
55
 
33
- | Property | Type | Description |
34
- |----------|------|-------------|
35
- | `deductCredit` | `(cost?: number) => Promise<boolean>` | Deduct credits (defaults to 1) |
36
- | `deductCredits` | `(cost: number) => Promise<boolean>` | Deduct specific amount |
37
- | `isDeducting` | `boolean` | Mutation is in progress |
56
+ ### CRITICAL SAFETY
38
57
 
39
- ## Basic Usage
58
+ - **ALWAYS** check return value before proceeding with feature execution
59
+ - **ALWAYS** handle the case where deduction returns `false`
60
+ - **NEVER** assume deduction succeeded without checking return value
61
+ - **MUST** implement error boundaries when using this hook
62
+
63
+ ## Rules
64
+
65
+ ### Initialization
40
66
 
41
67
  ```typescript
42
- function CreditButton() {
43
- const { user } = useAuth();
44
- const { deductCredit, isDeducting } = useDeductCredit({
45
- userId: user?.uid,
46
- onCreditsExhausted: () => {
47
- Alert.alert('Low Credits', 'Please purchase more credits');
48
- },
49
- });
50
-
51
- const handleUseFeature = async () => {
52
- const success = await deductCredit(1);
53
- if (success) {
54
- executePremiumFeature();
55
- }
56
- };
57
-
58
- return (
59
- <Button onPress={handleUseFeature} disabled={isDeducting}>
60
- Use Feature (1 Credit)
61
- </Button>
62
- );
63
- }
68
+ // CORRECT
69
+ const { deductCredit, isDeducting } = useDeductCredit({
70
+ userId: user?.uid,
71
+ onCreditsExhausted: () => showPaywall(),
72
+ });
73
+
74
+ // INCORRECT - Missing callback
75
+ const { deductCredit } = useDeductCredit({
76
+ userId: user?.uid,
77
+ });
78
+
79
+ // INCORRECT - userId undefined
80
+ const { deductCredit } = useDeductCredit({
81
+ userId: undefined,
82
+ });
64
83
  ```
65
84
 
66
- ## Advanced Usage
67
-
68
- ### With Custom Cost
85
+ ### Deduction Execution
69
86
 
70
87
  ```typescript
71
- function FeatureWithCost() {
72
- const { deductCredit } = useDeductCredit({
73
- userId: user?.uid,
74
- });
75
-
76
- const features = {
77
- basic: { cost: 1, name: 'Basic Generation' },
78
- advanced: { cost: 5, name: 'Advanced Generation' },
79
- premium: { cost: 10, name: 'Premium Generation' },
80
- };
81
-
82
- const handleFeature = async (cost: number) => {
83
- const success = await deductCredit(cost);
84
- if (success) {
85
- console.log('Feature used');
86
- }
87
- };
88
-
89
- return (
90
- <View>
91
- {Object.entries(features).map(([key, { cost, name }]) => (
92
- <Button
93
- key={key}
94
- onPress={() => handleFeature(cost)}
95
- title={`${name} (${cost} credits)`}
96
- />
97
- ))}
98
- </View>
99
- );
88
+ // CORRECT - Check return value
89
+ const success = await deductCredit(5);
90
+ if (success) {
91
+ executeFeature();
100
92
  }
93
+
94
+ // INCORRECT - No return value check
95
+ await deductCredit(5);
96
+ executeFeature(); // May execute even if deduction failed
97
+
98
+ // INCORRECT - Multiple simultaneous calls
99
+ Promise.all([
100
+ deductCredit(5),
101
+ deductCredit(3), // Race condition!
102
+ ]);
101
103
  ```
102
104
 
103
- ### With Confirmation
105
+ ### Loading States
104
106
 
105
107
  ```typescript
106
- function CreditDeductionWithConfirmation() {
107
- const { deductCredit } = useDeductCredit({
108
- userId: user?.uid,
109
- });
110
-
111
- const handleActionWithConfirmation = async (cost: number, action: string) => {
112
- Alert.alert(
113
- 'Confirm Action',
114
- `This will cost ${cost} credit${cost > 1 ? 's' : ''}. Continue?`,
115
- [
116
- { text: 'Cancel', style: 'cancel' },
117
- {
118
- text: 'Confirm',
119
- onPress: async () => {
120
- const success = await deductCredit(cost);
121
- if (success) {
122
- await performAction(action);
123
- }
124
- },
125
- },
126
- ]
127
- );
128
- };
129
-
130
- return (
131
- <Button
132
- onPress={() => handleActionWithConfirmation(5, 'export')}
133
- title="Export Data (5 Credits)"
134
- />
135
- );
136
- }
108
+ // CORRECT - Respect loading state
109
+ <Button onPress={handleDeduct} disabled={isDeducting}>
110
+ {isDeducting ? 'Deducting...' : 'Use Feature'}
111
+ </Button>
112
+
113
+ // INCORRECT - Ignore loading state
114
+ <Button onPress={handleDeduct}>
115
+ Use Feature
116
+ </Button>
137
117
  ```
138
118
 
139
- ## Best Practices
119
+ ### Credit Exhaustion Handling
120
+
121
+ ```typescript
122
+ // CORRECT - Implement callback
123
+ const { deductCredit } = useDeductCredit({
124
+ userId: user?.uid,
125
+ onCreditsExhausted: () => {
126
+ navigation.navigate('CreditPackages');
127
+ },
128
+ });
129
+
130
+ // MINIMUM - Show alert
131
+ const { deductCredit } = useDeductCredit({
132
+ userId: user?.uid,
133
+ onCreditsExhausted: () => {
134
+ Alert.alert('No Credits', 'Please purchase more credits');
135
+ },
136
+ });
137
+ ```
138
+
139
+ ## AI Agent Guidelines
140
+
141
+ ### When Implementing Features
142
+
143
+ 1. **Always** check if user has sufficient credits BEFORE allowing action
144
+ 2. **Always** show the credit cost to user before deducting
145
+ 3. **Always** disable buttons while `isDeducting` is true
146
+ 4. **Always** handle the case where deduction returns false
147
+ 5. **Never** allow zero or negative credit costs
148
+
149
+ ### Integration Checklist
140
150
 
141
- 1. **Check balance first** - Show user if they have enough credits
142
- 2. **Handle exhausted credits** - Provide purchase option
143
- 3. **Show loading state** - Disable button during deduction
144
- 4. **Optimistic updates** - UI updates automatically
145
- 5. **Error handling** - Handle deduction failures gracefully
146
- 6. **Confirm expensive actions** - Ask before large deductions
147
- 7. **Clear messaging** - Tell users cost before deducting
151
+ - [ ] Import from correct path: `@umituz/react-native-subscription`
152
+ - [ ] Provide valid `userId` from authentication context
153
+ - [ ] Implement `onCreditsExhausted` callback
154
+ - [ ] Check `isDeducting` before calling deduct functions
155
+ - [ ] Validate return value before proceeding
156
+ - [ ] Show credit cost to user before action
157
+ - [ ] Handle error cases gracefully
158
+ - [ ] Test with insufficient credits
159
+ - [ ] Test with zero balance
148
160
 
149
- ## Related Hooks
161
+ ### Common Patterns to Implement
150
162
 
151
- - **useCredits** - For accessing credits balance
152
- - **useCreditsGate** - For gating features with credits
153
- - **useInitializeCredits** - For initializing credits after purchase
154
- - **useCreditChecker** - For checking credit availability
163
+ 1. **Pre-check**: Use `useCreditChecker` before showing feature button
164
+ 2. **Confirmation Dialog**: Ask user before expensive operations (>5 credits)
165
+ 3. **Success Feedback**: Show success message after deduction
166
+ 4. **Failure Handling**: Show appropriate error message on failure
167
+ 5. **Purchase Flow**: Navigate to purchase screen on exhaustion
155
168
 
156
- ## See Also
169
+ ## Related Documentation
157
170
 
158
- - [Credits README](../../../domains/wallet/README.md)
159
- - [Credits Entity](../../../domains/wallet/domain/entities/Credits.md)
160
- - [Credits Repository](../../../infrastructure/repositories/CreditsRepository.md)
171
+ - **useCredits**: Access current credit balance
172
+ - **useCreditChecker**: Check credit availability before operations
173
+ - **useInitializeCredits**: Initialize credits after purchase
174
+ - **useFeatureGate**: Unified feature gating with credits
175
+ - **Credits Repository**: `src/domains/wallet/infrastructure/repositories/README.md`
176
+ - **Wallet Domain**: `src/domains/wallet/README.md`