@influto/react-native-sdk 1.0.0 → 1.2.0

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,596 @@
1
+ /**
2
+ * ReferralCodeInput - Pre-built UI component for promo code entry
3
+ *
4
+ * Features:
5
+ * - Auto-prefills if user came via attribution link
6
+ * - Real-time validation
7
+ * - Customizable styling (colors, fonts, spacing)
8
+ * - Loading & success/error states
9
+ * - Optional skip button
10
+ * - Callback on validation
11
+ * - Fully accessible
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { ReferralCodeInput } from '@influto/react-native-sdk/ui';
16
+ *
17
+ * <ReferralCodeInput
18
+ * onValidated={(result) => {
19
+ * if (result.valid) {
20
+ * navigation.navigate('Paywall', { campaign: result.campaign });
21
+ * }
22
+ * }}
23
+ * onSkip={() => navigation.navigate('Paywall')}
24
+ * />
25
+ * ```
26
+ */
27
+
28
+ import React, { useState, useEffect } from 'react';
29
+ import {
30
+ View,
31
+ TextInput,
32
+ Text,
33
+ TouchableOpacity,
34
+ ActivityIndicator,
35
+ StyleSheet,
36
+ ViewStyle,
37
+ TextStyle,
38
+ Platform
39
+ } from 'react-native';
40
+ import InfluTo from '../InfluTo';
41
+ import type { CodeValidationResult } from '../types';
42
+
43
+ export interface ReferralCodeInputProps {
44
+ /**
45
+ * Auto-prefill code if user came via attribution link
46
+ * @default true
47
+ */
48
+ autoPrefill?: boolean;
49
+
50
+ /**
51
+ * Auto-validate code on mount (if prefilled)
52
+ * @default false
53
+ */
54
+ autoValidate?: boolean;
55
+
56
+ /**
57
+ * Callback when code is validated (valid or invalid)
58
+ */
59
+ onValidated?: (result: CodeValidationResult) => void;
60
+
61
+ /**
62
+ * Callback when user skips code entry
63
+ */
64
+ onSkip?: () => void;
65
+
66
+ /**
67
+ * Callback when code is successfully applied
68
+ */
69
+ onApplied?: (result: CodeValidationResult) => void;
70
+
71
+ /**
72
+ * App user ID (if available) - used for attribution tracking
73
+ */
74
+ appUserId?: string;
75
+
76
+ /**
77
+ * Custom color scheme
78
+ */
79
+ colors?: {
80
+ primary?: string;
81
+ success?: string;
82
+ error?: string;
83
+ text?: string;
84
+ textSecondary?: string;
85
+ background?: string;
86
+ border?: string;
87
+ inputBackground?: string;
88
+ };
89
+
90
+ /**
91
+ * Custom fonts
92
+ */
93
+ fonts?: {
94
+ family?: string;
95
+ sizeTitle?: number;
96
+ sizeInput?: number;
97
+ sizeButton?: number;
98
+ sizeMessage?: number;
99
+ };
100
+
101
+ /**
102
+ * Custom text labels (for internationalization)
103
+ */
104
+ labels?: {
105
+ title?: string;
106
+ subtitle?: string;
107
+ placeholder?: string;
108
+ validateButton?: string;
109
+ skipButton?: string;
110
+ validatingMessage?: string;
111
+ validMessage?: string;
112
+ invalidMessage?: string;
113
+ errorMessage?: string;
114
+ prefilledMessage?: string;
115
+ };
116
+
117
+ /**
118
+ * Custom styles for fine-grained control
119
+ */
120
+ style?: {
121
+ container?: ViewStyle;
122
+ titleContainer?: ViewStyle;
123
+ title?: TextStyle;
124
+ subtitle?: TextStyle;
125
+ inputContainer?: ViewStyle;
126
+ input?: TextStyle;
127
+ buttonContainer?: ViewStyle;
128
+ validateButton?: ViewStyle;
129
+ validateButtonText?: TextStyle;
130
+ skipButton?: ViewStyle;
131
+ skipButtonText?: TextStyle;
132
+ messageContainer?: ViewStyle;
133
+ messageText?: TextStyle;
134
+ };
135
+
136
+ /**
137
+ * Show skip button
138
+ * @default true
139
+ */
140
+ showSkipButton?: boolean;
141
+
142
+ /**
143
+ * Validate on blur (when user leaves input)
144
+ * @default true
145
+ */
146
+ validateOnBlur?: boolean;
147
+ }
148
+
149
+ type ValidationState = 'idle' | 'validating' | 'valid' | 'invalid' | 'error';
150
+
151
+ export const ReferralCodeInput: React.FC<ReferralCodeInputProps> = ({
152
+ autoPrefill = true,
153
+ autoValidate = false,
154
+ onValidated,
155
+ onSkip,
156
+ onApplied,
157
+ appUserId,
158
+ colors = {},
159
+ fonts = {},
160
+ labels = {},
161
+ style = {},
162
+ showSkipButton = true,
163
+ validateOnBlur = true
164
+ }) => {
165
+ const [code, setCode] = useState('');
166
+ const [state, setState] = useState<ValidationState>('idle');
167
+ const [validationResult, setValidationResult] = useState<CodeValidationResult | null>(null);
168
+ const [isPrefilled, setIsPrefilled] = useState(false);
169
+
170
+ // Default colors
171
+ const colorScheme = {
172
+ primary: colors.primary || '#3B82F6',
173
+ success: colors.success || '#10B981',
174
+ error: colors.error || '#EF4444',
175
+ text: colors.text || '#1F2937',
176
+ textSecondary: colors.textSecondary || '#6B7280',
177
+ background: colors.background || '#FFFFFF',
178
+ border: colors.border || '#D1D5DB',
179
+ inputBackground: colors.inputBackground || '#F9FAFB'
180
+ };
181
+
182
+ // Default fonts
183
+ const fontScheme = {
184
+ family: fonts.family || (Platform.OS === 'ios' ? 'System' : 'Roboto'),
185
+ sizeTitle: fonts.sizeTitle || 18,
186
+ sizeInput: fonts.sizeInput || 16,
187
+ sizeButton: fonts.sizeButton || 16,
188
+ sizeMessage: fonts.sizeMessage || 14
189
+ };
190
+
191
+ // Default labels
192
+ const labelScheme = {
193
+ title: labels.title || 'Have a promo code?',
194
+ subtitle: labels.subtitle || 'Enter your referral code to unlock special offers',
195
+ placeholder: labels.placeholder || 'Enter code (e.g., FITGURU30)',
196
+ validateButton: labels.validateButton || 'Apply Code',
197
+ skipButton: labels.skipButton || 'Skip',
198
+ validatingMessage: labels.validatingMessage || 'Validating code...',
199
+ validMessage: labels.validMessage || 'Code applied successfully!',
200
+ invalidMessage: labels.invalidMessage || 'Invalid code. Please try again.',
201
+ errorMessage: labels.errorMessage || 'Unable to validate code. Check your connection.',
202
+ prefilledMessage: labels.prefilledMessage || 'Code detected from your referral link'
203
+ };
204
+
205
+ // Auto-prefill on mount
206
+ useEffect(() => {
207
+ if (autoPrefill) {
208
+ loadPrefilledCode();
209
+ }
210
+ }, [autoPrefill]);
211
+
212
+ const loadPrefilledCode = async () => {
213
+ try {
214
+ const prefilledCode = await InfluTo.getPrefilledCode();
215
+
216
+ if (prefilledCode) {
217
+ setCode(prefilledCode);
218
+ setIsPrefilled(true);
219
+
220
+ // Auto-validate if enabled
221
+ if (autoValidate) {
222
+ await handleValidate(prefilledCode);
223
+ }
224
+ }
225
+ } catch (error) {
226
+ console.error('[ReferralCodeInput] Failed to load prefilled code:', error);
227
+ }
228
+ };
229
+
230
+ const handleValidate = async (codeToValidate: string = code) => {
231
+ if (!codeToValidate || codeToValidate.length < 4) {
232
+ setState('invalid');
233
+ setValidationResult({
234
+ valid: false,
235
+ error: 'Please enter a valid code',
236
+ error_code: 'INVALID_FORMAT'
237
+ });
238
+ return;
239
+ }
240
+
241
+ setState('validating');
242
+ setValidationResult(null);
243
+
244
+ try {
245
+ const result = await InfluTo.validateCode(codeToValidate);
246
+ setValidationResult(result);
247
+
248
+ if (result.valid) {
249
+ setState('valid');
250
+
251
+ // Automatically set the code if valid
252
+ const setResult = await InfluTo.setReferralCode(codeToValidate, appUserId);
253
+
254
+ if (setResult.success) {
255
+ onApplied?.(result);
256
+ }
257
+ } else {
258
+ setState('invalid');
259
+ }
260
+
261
+ // Notify parent
262
+ onValidated?.(result);
263
+ } catch (error) {
264
+ setState('error');
265
+ setValidationResult({
266
+ valid: false,
267
+ error: 'Network error',
268
+ error_code: 'NETWORK_ERROR'
269
+ });
270
+
271
+ onValidated?.({
272
+ valid: false,
273
+ error: 'Network error',
274
+ error_code: 'NETWORK_ERROR'
275
+ });
276
+ }
277
+ };
278
+
279
+ const handleSkip = () => {
280
+ onSkip?.();
281
+ };
282
+
283
+ const handleBlur = () => {
284
+ if (validateOnBlur && code && state === 'idle') {
285
+ handleValidate();
286
+ }
287
+ };
288
+
289
+ // Get message based on state
290
+ const getMessage = () => {
291
+ if (isPrefilled && state === 'idle') {
292
+ return { text: labelScheme.prefilledMessage, color: colorScheme.primary };
293
+ }
294
+
295
+ switch (state) {
296
+ case 'validating':
297
+ return { text: labelScheme.validatingMessage, color: colorScheme.textSecondary };
298
+ case 'valid':
299
+ return {
300
+ text: validationResult?.message || labelScheme.validMessage,
301
+ color: colorScheme.success
302
+ };
303
+ case 'invalid':
304
+ return {
305
+ text: validationResult?.error || labelScheme.invalidMessage,
306
+ color: colorScheme.error
307
+ };
308
+ case 'error':
309
+ return { text: labelScheme.errorMessage, color: colorScheme.error };
310
+ default:
311
+ return null;
312
+ }
313
+ };
314
+
315
+ const message = getMessage();
316
+
317
+ return (
318
+ <View style={[styles.container, style.container]}>
319
+ {/* Title */}
320
+ <View style={[styles.titleContainer, style.titleContainer]}>
321
+ <Text style={[
322
+ styles.title,
323
+ { color: colorScheme.text, fontFamily: fontScheme.family, fontSize: fontScheme.sizeTitle },
324
+ style.title
325
+ ]}>
326
+ {labelScheme.title}
327
+ </Text>
328
+ {labelScheme.subtitle && (
329
+ <Text style={[
330
+ styles.subtitle,
331
+ { color: colorScheme.textSecondary, fontFamily: fontScheme.family, fontSize: fontScheme.sizeMessage },
332
+ style.subtitle
333
+ ]}>
334
+ {labelScheme.subtitle}
335
+ </Text>
336
+ )}
337
+ </View>
338
+
339
+ {/* Input */}
340
+ <View style={[styles.inputContainer, style.inputContainer]}>
341
+ <TextInput
342
+ style={[
343
+ styles.input,
344
+ {
345
+ backgroundColor: colorScheme.inputBackground,
346
+ borderColor: state === 'valid' ? colorScheme.success : state === 'invalid' || state === 'error' ? colorScheme.error : colorScheme.border,
347
+ color: colorScheme.text,
348
+ fontFamily: fontScheme.family,
349
+ fontSize: fontScheme.sizeInput
350
+ },
351
+ style.input
352
+ ]}
353
+ value={code}
354
+ onChangeText={(text) => {
355
+ setCode(text.toUpperCase());
356
+ if (state !== 'idle' && state !== 'validating') {
357
+ setState('idle');
358
+ setValidationResult(null);
359
+ }
360
+ }}
361
+ onBlur={handleBlur}
362
+ placeholder={labelScheme.placeholder}
363
+ placeholderTextColor={colorScheme.textSecondary}
364
+ autoCapitalize="characters"
365
+ autoCorrect={false}
366
+ autoComplete="off"
367
+ maxLength={20}
368
+ editable={state !== 'validating'}
369
+ accessibilityLabel="Referral code input"
370
+ accessibilityHint="Enter your promo or referral code"
371
+ />
372
+
373
+ {/* Success/Error Icon */}
374
+ {state === 'valid' && (
375
+ <View style={styles.iconContainer}>
376
+ <Text style={[styles.icon, { color: colorScheme.success }]}>✓</Text>
377
+ </View>
378
+ )}
379
+ {(state === 'invalid' || state === 'error') && (
380
+ <View style={styles.iconContainer}>
381
+ <Text style={[styles.icon, { color: colorScheme.error }]}>✗</Text>
382
+ </View>
383
+ )}
384
+ </View>
385
+
386
+ {/* Validation Message */}
387
+ {message && (
388
+ <View style={[styles.messageContainer, style.messageContainer]}>
389
+ <Text style={[
390
+ styles.messageText,
391
+ { color: message.color, fontFamily: fontScheme.family, fontSize: fontScheme.sizeMessage },
392
+ style.messageText
393
+ ]}>
394
+ {message.text}
395
+ </Text>
396
+ </View>
397
+ )}
398
+
399
+ {/* Buttons */}
400
+ <View style={[styles.buttonContainer, style.buttonContainer]}>
401
+ {/* Validate Button */}
402
+ <TouchableOpacity
403
+ style={[
404
+ styles.validateButton,
405
+ {
406
+ backgroundColor: state === 'valid' ? colorScheme.success : colorScheme.primary,
407
+ opacity: state === 'validating' || !code ? 0.6 : 1
408
+ },
409
+ style.validateButton
410
+ ]}
411
+ onPress={() => handleValidate()}
412
+ disabled={state === 'validating' || !code}
413
+ accessibilityLabel={labelScheme.validateButton}
414
+ accessibilityRole="button"
415
+ >
416
+ {state === 'validating' ? (
417
+ <ActivityIndicator color="#FFFFFF" size="small" />
418
+ ) : (
419
+ <Text style={[
420
+ styles.validateButtonText,
421
+ { fontFamily: fontScheme.family, fontSize: fontScheme.sizeButton },
422
+ style.validateButtonText
423
+ ]}>
424
+ {state === 'valid' ? '✓ ' : ''}{labelScheme.validateButton}
425
+ </Text>
426
+ )}
427
+ </TouchableOpacity>
428
+
429
+ {/* Skip Button */}
430
+ {showSkipButton && onSkip && (
431
+ <TouchableOpacity
432
+ style={[styles.skipButton, style.skipButton]}
433
+ onPress={handleSkip}
434
+ disabled={state === 'validating'}
435
+ accessibilityLabel={labelScheme.skipButton}
436
+ accessibilityRole="button"
437
+ >
438
+ <Text style={[
439
+ styles.skipButtonText,
440
+ { color: colorScheme.textSecondary, fontFamily: fontScheme.family, fontSize: fontScheme.sizeButton },
441
+ style.skipButtonText
442
+ ]}>
443
+ {labelScheme.skipButton}
444
+ </Text>
445
+ </TouchableOpacity>
446
+ )}
447
+ </View>
448
+
449
+ {/* Campaign Info (if valid) */}
450
+ {state === 'valid' && validationResult?.campaign && (
451
+ <View style={styles.campaignInfo}>
452
+ <Text style={[
453
+ styles.campaignTitle,
454
+ { color: colorScheme.text, fontFamily: fontScheme.family }
455
+ ]}>
456
+ {validationResult.campaign.name}
457
+ </Text>
458
+ {validationResult.campaign.description && (
459
+ <Text style={[
460
+ styles.campaignDescription,
461
+ { color: colorScheme.textSecondary, fontFamily: fontScheme.family }
462
+ ]}>
463
+ {validationResult.campaign.description}
464
+ </Text>
465
+ )}
466
+ {validationResult.influencer && (
467
+ <Text style={[
468
+ styles.influencerInfo,
469
+ { color: colorScheme.textSecondary, fontFamily: fontScheme.family }
470
+ ]}>
471
+ Referred by {validationResult.influencer.name}
472
+ {validationResult.influencer.social_handle && ` (@${validationResult.influencer.social_handle})`}
473
+ </Text>
474
+ )}
475
+ </View>
476
+ )}
477
+ </View>
478
+ );
479
+ };
480
+
481
+ const styles = StyleSheet.create({
482
+ container: {
483
+ padding: 20,
484
+ backgroundColor: '#FFFFFF',
485
+ borderRadius: 12,
486
+ shadowColor: '#000',
487
+ shadowOffset: { width: 0, height: 2 },
488
+ shadowOpacity: 0.1,
489
+ shadowRadius: 8,
490
+ elevation: 4
491
+ },
492
+ titleContainer: {
493
+ marginBottom: 16
494
+ },
495
+ title: {
496
+ fontSize: 18,
497
+ fontWeight: '600',
498
+ marginBottom: 4
499
+ },
500
+ subtitle: {
501
+ fontSize: 14,
502
+ lineHeight: 20
503
+ },
504
+ inputContainer: {
505
+ position: 'relative',
506
+ marginBottom: 12
507
+ },
508
+ input: {
509
+ height: 50,
510
+ borderWidth: 2,
511
+ borderRadius: 8,
512
+ paddingHorizontal: 16,
513
+ paddingRight: 48, // Space for icon
514
+ fontSize: 16,
515
+ fontWeight: '500'
516
+ },
517
+ iconContainer: {
518
+ position: 'absolute',
519
+ right: 16,
520
+ top: 12,
521
+ width: 24,
522
+ height: 24,
523
+ justifyContent: 'center',
524
+ alignItems: 'center'
525
+ },
526
+ icon: {
527
+ fontSize: 20,
528
+ fontWeight: 'bold'
529
+ },
530
+ messageContainer: {
531
+ marginBottom: 12,
532
+ padding: 12,
533
+ backgroundColor: '#F9FAFB',
534
+ borderRadius: 6
535
+ },
536
+ messageText: {
537
+ fontSize: 14,
538
+ lineHeight: 18,
539
+ textAlign: 'center'
540
+ },
541
+ buttonContainer: {
542
+ flexDirection: 'row',
543
+ gap: 12
544
+ },
545
+ validateButton: {
546
+ flex: 1,
547
+ height: 50,
548
+ borderRadius: 8,
549
+ justifyContent: 'center',
550
+ alignItems: 'center',
551
+ shadowColor: '#000',
552
+ shadowOffset: { width: 0, height: 2 },
553
+ shadowOpacity: 0.1,
554
+ shadowRadius: 4,
555
+ elevation: 2
556
+ },
557
+ validateButtonText: {
558
+ color: '#FFFFFF',
559
+ fontSize: 16,
560
+ fontWeight: '600'
561
+ },
562
+ skipButton: {
563
+ height: 50,
564
+ paddingHorizontal: 20,
565
+ justifyContent: 'center',
566
+ alignItems: 'center'
567
+ },
568
+ skipButtonText: {
569
+ fontSize: 16,
570
+ fontWeight: '500'
571
+ },
572
+ campaignInfo: {
573
+ marginTop: 16,
574
+ padding: 12,
575
+ backgroundColor: '#F0FDF4',
576
+ borderRadius: 8,
577
+ borderLeftWidth: 3,
578
+ borderLeftColor: '#10B981'
579
+ },
580
+ campaignTitle: {
581
+ fontSize: 15,
582
+ fontWeight: '600',
583
+ marginBottom: 4
584
+ },
585
+ campaignDescription: {
586
+ fontSize: 13,
587
+ lineHeight: 18,
588
+ marginBottom: 4
589
+ },
590
+ influencerInfo: {
591
+ fontSize: 12,
592
+ fontStyle: 'italic'
593
+ }
594
+ });
595
+
596
+ export default ReferralCodeInput;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * InfluTo SDK UI Components
3
+ *
4
+ * Pre-built React Native components for easy integration
5
+ */
6
+
7
+ export { ReferralCodeInput } from './ReferralCodeInput';
8
+ export type { ReferralCodeInputProps } from './ReferralCodeInput';
package/src/index.ts CHANGED
@@ -5,3 +5,6 @@
5
5
 
6
6
  export { default } from './InfluTo';
7
7
  export * from './types';
8
+
9
+ // Note: UI components are exported separately via './ui' to keep bundle size small
10
+ // Import with: import { ReferralCodeInput } from '@influto/react-native-sdk/ui';
package/src/types.ts CHANGED
@@ -89,3 +89,80 @@ export interface DeviceInfo {
89
89
  timezone?: string;
90
90
  language?: string;
91
91
  }
92
+
93
+ export interface CodeValidationResult {
94
+ /**
95
+ * Whether the code is valid
96
+ */
97
+ valid: boolean;
98
+
99
+ /**
100
+ * Normalized code (uppercase)
101
+ */
102
+ code?: string;
103
+
104
+ /**
105
+ * Campaign information if valid
106
+ */
107
+ campaign?: {
108
+ id: string;
109
+ name: string;
110
+ description?: string;
111
+ commission_percentage: number;
112
+ campaign_type: string;
113
+ };
114
+
115
+ /**
116
+ * Influencer information if available
117
+ */
118
+ influencer?: {
119
+ name: string;
120
+ social_handle?: string;
121
+ follower_count?: number;
122
+ };
123
+
124
+ /**
125
+ * Custom campaign data (for conditional offers)
126
+ */
127
+ custom_data?: Record<string, any>;
128
+
129
+ /**
130
+ * Success message
131
+ */
132
+ message?: string;
133
+
134
+ /**
135
+ * Error message if invalid
136
+ */
137
+ error?: string;
138
+
139
+ /**
140
+ * Error code for programmatic handling
141
+ */
142
+ error_code?: 'INVALID_FORMAT' | 'CODE_NOT_FOUND' | 'NETWORK_ERROR';
143
+ }
144
+
145
+ export interface SetCodeResult {
146
+ /**
147
+ * Whether code was set successfully
148
+ */
149
+ success: boolean;
150
+
151
+ /**
152
+ * Normalized code
153
+ */
154
+ code?: string;
155
+
156
+ /**
157
+ * Success/error message
158
+ */
159
+ message: string;
160
+
161
+ /**
162
+ * Campaign info
163
+ */
164
+ campaign?: {
165
+ id: string;
166
+ name: string;
167
+ };
168
+ }
package/src/ui.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * InfluTo SDK - UI Components Entry Point
3
+ *
4
+ * Import UI components separately to keep main SDK bundle small.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { ReferralCodeInput } from '@influto/react-native-sdk/ui';
9
+ * ```
10
+ */
11
+
12
+ export * from './components';