@influto/react-native-sdk 1.1.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.
- package/README.md +50 -2
- package/lib/InfluTo.d.ts +82 -1
- package/lib/InfluTo.js +179 -0
- package/lib/components/ReferralCodeInput.d.ts +125 -0
- package/lib/components/ReferralCodeInput.js +397 -0
- package/lib/components/index.d.ts +7 -0
- package/lib/components/index.js +10 -0
- package/lib/index.js +2 -0
- package/lib/types.d.ts +65 -0
- package/lib/ui.d.ts +11 -0
- package/lib/ui.js +27 -0
- package/package.json +13 -1
- package/src/InfluTo.ts +199 -1
- package/src/components/ReferralCodeInput.tsx +596 -0
- package/src/components/index.ts +8 -0
- package/src/index.ts +3 -0
- package/src/types.ts +77 -0
- package/src/ui.ts +12 -0
|
@@ -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;
|
package/src/index.ts
CHANGED
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';
|