@coinbase/create-cdp-app 0.0.54 β†’ 0.0.55

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.
@@ -1,258 +1,77 @@
1
- import React, { useRef, useEffect } from "react";
1
+ import React, { useState } from "react";
2
2
  import {
3
3
  View,
4
4
  TouchableOpacity,
5
5
  Text,
6
- TextInput,
7
- KeyboardAvoidingView,
8
- Platform,
9
6
  StyleSheet,
10
- ScrollView,
11
7
  Dimensions,
12
- Keyboard,
8
+ Platform,
9
+ ScrollView,
10
+ KeyboardAvoidingView,
13
11
  } from "react-native";
14
- import { useTheme } from "../theme/ThemeContext";
15
12
  import { AuthMethod } from "../types";
13
+ import { useTheme } from "../theme/ThemeContext";
14
+ import { EmailForm } from "./EmailForm";
15
+ import { SmsForm } from "./SmsForm";
16
+ import { OAuthForm } from "./OAuthForm";
17
+ import { OtpForm } from "./OtpForm";
16
18
 
17
- export interface SignInFormProps {
18
- authMethod: AuthMethod;
19
- onAuthMethodToggle: () => void;
20
- email: string;
21
- setEmail: (email: string) => void;
22
- phoneNumber: string;
23
- setPhoneNumber: (phoneNumber: string) => void;
24
- otp: string;
25
- setOtp: (otp: string) => void;
26
- flowId: string;
27
- isLoading: boolean;
28
- onSignIn: () => void;
29
- onVerifyOTP: () => void;
30
- onBack?: () => void;
31
- }
19
+ export interface SignInFormProps {}
32
20
 
33
- export const SignInForm: React.FC<SignInFormProps> = ({
34
- authMethod,
35
- onAuthMethodToggle,
36
- email,
37
- setEmail,
38
- phoneNumber,
39
- setPhoneNumber,
40
- otp,
41
- setOtp,
42
- flowId,
43
- isLoading,
44
- onSignIn,
45
- onVerifyOTP,
46
- onBack,
47
- }) => {
21
+ export const SignInForm: React.FC<SignInFormProps> = () => {
48
22
  const { colors } = useTheme();
49
- const scrollViewRef = useRef<ScrollView>(null);
23
+ const [authMethod, setAuthMethod] = useState<AuthMethod>("email");
24
+ const [flowId, setFlowId] = useState("");
50
25
 
51
26
  const { width: screenWidth, height: screenHeight } = Dimensions.get("window");
52
27
  const isSmallScreen = screenHeight < 700;
53
28
 
54
- // Auto-scroll to ensure OTP field and button are visible
55
- useEffect(() => {
56
- if (flowId && scrollViewRef.current) {
57
- // Initial scroll when OTP field appears
58
- setTimeout(() => {
59
- scrollViewRef.current?.scrollTo({
60
- y: 200, // Scroll up enough to show both OTP field and button
61
- animated: true,
62
- });
63
- }, 300);
64
- }
65
- }, [flowId]);
66
-
67
- // Handle keyboard events for better positioning
68
- useEffect(() => {
69
- const keyboardDidShowListener = Keyboard.addListener("keyboardDidShow", () => {
70
- if (flowId && scrollViewRef.current) {
71
- setTimeout(() => {
72
- scrollViewRef.current?.scrollTo({
73
- y: 250, // Scroll up more when keyboard is visible
74
- animated: true,
75
- });
76
- }, 100);
77
- }
78
- });
29
+ const handleSignInSuccess = (newFlowId: string) => {
30
+ setFlowId(newFlowId);
31
+ };
79
32
 
80
- const keyboardDidHideListener = Keyboard.addListener("keyboardDidHide", () => {
81
- if (flowId && scrollViewRef.current) {
82
- setTimeout(() => {
83
- scrollViewRef.current?.scrollTo({
84
- y: 150, // Scroll back down a bit when keyboard hides
85
- animated: true,
86
- });
87
- }, 100);
88
- }
89
- });
33
+ const handleVerifySuccess = () => {
34
+ setFlowId("");
35
+ };
90
36
 
91
- return () => {
92
- keyboardDidShowListener.remove();
93
- keyboardDidHideListener.remove();
94
- };
95
- }, [flowId]);
37
+ const handleBack = () => {
38
+ setFlowId("");
39
+ };
96
40
 
97
41
  const createStyles = () =>
98
42
  StyleSheet.create({
99
- keyboardContainer: {
43
+ container: {
100
44
  flex: 1,
101
45
  },
102
- scrollContainer: {
103
- flexGrow: 1,
104
- justifyContent: "center",
46
+ tabContainer: {
47
+ flexDirection: "row",
105
48
  paddingHorizontal: 20,
106
- paddingVertical: isSmallScreen ? 20 : 40,
107
- paddingBottom: flowId ? (isSmallScreen ? 150 : 180) : isSmallScreen ? 20 : 40,
108
- minHeight: screenHeight - (Platform.OS === "ios" ? 100 : 80),
109
- },
110
- container: {
111
- alignItems: "center",
112
- justifyContent: "center",
113
- },
114
- card: {
115
- backgroundColor: colors.cardBackground,
116
- borderRadius: 16,
117
- padding: isSmallScreen ? 24 : 32,
118
- width: "100%",
119
- maxWidth: 400,
120
- minWidth: Math.min(screenWidth - 40, 280),
121
- shadowColor: "#000",
122
- shadowOffset: {
123
- width: 0,
124
- height: 2,
125
- },
126
- shadowOpacity: 0.1,
127
- shadowRadius: 8,
128
- elevation: 3,
129
- },
130
- header: {
131
- alignItems: "center",
132
- marginBottom: isSmallScreen ? 24 : 32,
133
- position: "relative",
134
- },
135
- backButton: {
136
- position: "absolute",
137
- left: 0,
138
- top: 0,
139
- width: 40,
140
- height: 40,
141
- borderRadius: 20,
142
- backgroundColor: colors.inputBackground,
143
- borderWidth: 1,
144
- borderColor: colors.border,
145
- justifyContent: "center",
146
- alignItems: "center",
147
- },
148
- backButtonText: {
149
- fontSize: 18,
150
- color: colors.text,
151
- fontWeight: "600",
152
- },
153
- logoCircle: {
154
- width: isSmallScreen ? 56 : 64,
155
- height: isSmallScreen ? 56 : 64,
156
- borderRadius: isSmallScreen ? 28 : 32,
157
- backgroundColor: colors.accent,
158
- justifyContent: "center",
159
- alignItems: "center",
160
- marginBottom: 16,
49
+ paddingTop: 20,
50
+ gap: 12,
161
51
  },
162
- logoText: {
163
- color: "#ffffff",
164
- fontSize: isSmallScreen ? 28 : 32,
165
- fontWeight: "bold",
166
- },
167
- title: {
168
- fontSize: isSmallScreen ? 20 : 24,
169
- fontWeight: "500",
170
- color: colors.text,
171
- textAlign: "center",
172
- },
173
- form: {
174
- width: "100%",
175
- },
176
- inputLabel: {
177
- fontSize: 16,
178
- fontWeight: "500",
179
- color: colors.text,
180
- marginBottom: 8,
181
- },
182
- input: {
183
- backgroundColor: colors.inputBackground,
52
+ tab: {
53
+ flex: 1,
54
+ paddingVertical: 12,
184
55
  borderRadius: 8,
185
56
  borderWidth: 1,
186
57
  borderColor: colors.border,
187
- paddingHorizontal: 16,
188
- paddingVertical: Platform.OS === "ios" ? 16 : 14,
189
- fontSize: 16,
190
- color: colors.text,
191
- marginBottom: isSmallScreen ? 20 : 24,
192
- minHeight: Platform.OS === "android" ? 48 : 44,
193
- },
194
- inputDisabled: {
195
- opacity: 0.6,
196
- backgroundColor: colors.border,
197
- },
198
- inputContainer: {
199
- flexDirection: "row",
200
- alignItems: "stretch",
201
- marginBottom: isSmallScreen ? 20 : 24,
202
- },
203
- flagContainer: {
204
- flexDirection: "row",
205
- alignItems: "center",
206
- paddingHorizontal: 12,
207
- paddingVertical: Platform.OS === "ios" ? 16 : 14,
208
- borderWidth: 1,
209
- borderColor: colors.border,
210
- borderTopLeftRadius: 8,
211
- borderBottomLeftRadius: 8,
212
58
  backgroundColor: colors.inputBackground,
213
- minHeight: Platform.OS === "android" ? 48 : 44,
214
- justifyContent: "center",
59
+ alignItems: "center",
215
60
  },
216
- flagText: {
217
- fontSize: 16,
218
- marginRight: 4,
61
+ tabActive: {
62
+ backgroundColor: colors.accent,
63
+ borderColor: colors.accent,
219
64
  },
220
- countryCode: {
221
- fontSize: 16,
222
- color: colors.text,
65
+ tabText: {
66
+ fontSize: 14,
223
67
  fontWeight: "500",
224
- },
225
- phoneInput: {
226
- backgroundColor: colors.inputBackground,
227
- borderRadius: 0,
228
- borderTopRightRadius: 8,
229
- borderBottomRightRadius: 8,
230
- borderWidth: 1,
231
- borderLeftWidth: 0,
232
- borderColor: colors.border,
233
- paddingHorizontal: 16,
234
- paddingVertical: Platform.OS === "ios" ? 16 : 14,
235
- fontSize: 16,
236
68
  color: colors.text,
237
- flex: 1,
238
- minHeight: Platform.OS === "android" ? 48 : 44,
239
- },
240
- continueButton: {
241
- backgroundColor: colors.accent,
242
- borderRadius: 8,
243
- paddingVertical: 16,
244
- alignItems: "center",
245
- marginBottom: isSmallScreen ? 20 : 24,
246
- minHeight: 48,
247
- justifyContent: "center",
248
69
  },
249
- continueButtonText: {
70
+ tabTextActive: {
250
71
  color: "#ffffff",
251
- fontSize: 16,
252
- fontWeight: "600",
253
72
  },
254
- buttonDisabled: {
255
- opacity: 0.6,
73
+ formContainer: {
74
+ flex: 1,
256
75
  },
257
76
  dividerContainer: {
258
77
  flexDirection: "row",
@@ -270,6 +89,9 @@ export const SignInForm: React.FC<SignInFormProps> = ({
270
89
  fontSize: 14,
271
90
  marginHorizontal: 16,
272
91
  },
92
+ toggleButtonContainer: {
93
+ gap: 12,
94
+ },
273
95
  toggleButton: {
274
96
  backgroundColor: colors.inputBackground,
275
97
  borderRadius: 8,
@@ -290,144 +112,110 @@ export const SignInForm: React.FC<SignInFormProps> = ({
290
112
  fontSize: 16,
291
113
  fontWeight: "500",
292
114
  },
115
+ loginContainer: {
116
+ alignItems: "center",
117
+ justifyContent: "center",
118
+ },
119
+ card: {
120
+ backgroundColor: colors.cardBackground,
121
+ borderRadius: 16,
122
+ padding: isSmallScreen ? 24 : 32,
123
+ width: "100%",
124
+ maxWidth: 400,
125
+ minWidth: Math.min(screenWidth - 40, 280),
126
+ minHeight: screenHeight - (Platform.OS === "ios" ? 350 : 300),
127
+ shadowColor: "#000",
128
+ shadowOffset: {
129
+ width: 0,
130
+ height: 2,
131
+ },
132
+ shadowOpacity: 0.1,
133
+ shadowRadius: 8,
134
+ elevation: 3,
135
+ },
136
+ keyboardContainer: {
137
+ flex: 1,
138
+ },
139
+ scrollContainer: {
140
+ flexGrow: 1,
141
+ justifyContent: "center",
142
+ paddingHorizontal: 20,
143
+ paddingVertical: isSmallScreen ? 20 : 40,
144
+ minHeight: screenHeight - (Platform.OS === "ios" ? 100 : 80),
145
+ },
293
146
  });
294
147
 
295
148
  const styles = createStyles();
296
149
 
297
- return (
298
- <KeyboardAvoidingView
299
- behavior={Platform.OS === "ios" ? "padding" : "position"}
300
- style={styles.keyboardContainer}
301
- keyboardVerticalOffset={Platform.OS === "ios" ? 60 : -150}
302
- >
303
- <ScrollView
304
- ref={scrollViewRef}
305
- contentContainerStyle={styles.scrollContainer}
306
- keyboardShouldPersistTaps="handled"
307
- showsVerticalScrollIndicator={false}
308
- >
309
- <View style={styles.container}>
310
- <View style={styles.card}>
311
- <View style={styles.header}>
312
- {flowId && onBack && (
313
- <TouchableOpacity style={styles.backButton} onPress={onBack} disabled={isLoading}>
314
- <Text style={styles.backButtonText}>←</Text>
315
- </TouchableOpacity>
316
- )}
317
- <View style={styles.logoCircle}>
318
- <Text style={styles.logoText}>C</Text>
319
- </View>
320
- <Text style={styles.title}>
321
- {flowId ? "Check your " + (authMethod === "email" ? "email" : "phone") : "Sign in"}
322
- </Text>
323
- </View>
150
+ // If we have a flowId, show the OTP form (without tabs)
151
+ if (flowId) {
152
+ return (
153
+ <OtpForm
154
+ authMethod={authMethod}
155
+ flowId={flowId}
156
+ onSuccess={handleVerifySuccess}
157
+ onBack={handleBack}
158
+ />
159
+ );
160
+ }
324
161
 
325
- <View style={styles.form}>
326
- {/* Email/Phone Input Section */}
327
- <Text style={styles.inputLabel}>
328
- {authMethod === "email" ? "Email address" : "Phone number"}
329
- </Text>
330
- {authMethod === "sms" ? (
331
- <View style={styles.inputContainer}>
332
- <View style={styles.flagContainer}>
333
- <Text style={styles.flagText}>πŸ‡ΊπŸ‡Έ</Text>
334
- <Text style={styles.countryCode}>+1</Text>
335
- </View>
336
- <TextInput
337
- style={[styles.phoneInput, flowId && styles.inputDisabled]}
338
- value={phoneNumber}
339
- onChangeText={setPhoneNumber}
340
- placeholder="(000) 000-0000"
341
- keyboardType="phone-pad"
342
- autoCapitalize="none"
343
- autoCorrect={false}
344
- editable={!isLoading && !flowId}
345
- placeholderTextColor={colors.textSecondary}
346
- returnKeyType="done"
347
- onSubmitEditing={flowId ? undefined : onSignIn}
348
- blurOnSubmit={true}
349
- />
162
+ // Otherwise show tabs and the selected form
163
+ return (
164
+ <View style={styles.container}>
165
+ <View style={styles.formContainer}>
166
+ <KeyboardAvoidingView
167
+ behavior={Platform.OS === "ios" ? "padding" : "position"}
168
+ style={styles.keyboardContainer}
169
+ keyboardVerticalOffset={Platform.OS === "ios" ? 60 : -150}
170
+ >
171
+ <ScrollView
172
+ contentContainerStyle={styles.scrollContainer}
173
+ keyboardShouldPersistTaps="handled"
174
+ showsVerticalScrollIndicator={false}
175
+ >
176
+ <View style={styles.loginContainer}>
177
+ <View style={styles.card}>
178
+ {authMethod === "email" && <EmailForm onSuccess={handleSignInSuccess} />}
179
+ {authMethod === "sms" && <SmsForm onSuccess={handleSignInSuccess} />}
180
+ {authMethod === "oauth" && <OAuthForm />}
181
+ <View style={styles.dividerContainer}>
182
+ <View style={styles.dividerLine} />
183
+ <Text style={styles.dividerText}>OR</Text>
184
+ <View style={styles.dividerLine} />
350
185
  </View>
351
- ) : (
352
- <TextInput
353
- style={[styles.input, flowId && styles.inputDisabled]}
354
- value={email}
355
- onChangeText={setEmail}
356
- placeholder="name@example.com"
357
- keyboardType="email-address"
358
- autoCapitalize="none"
359
- autoCorrect={false}
360
- editable={!isLoading && !flowId}
361
- placeholderTextColor={colors.textSecondary}
362
- returnKeyType="done"
363
- onSubmitEditing={flowId ? undefined : onSignIn}
364
- blurOnSubmit={true}
365
- />
366
- )}
367
-
368
- {/* OTP Input Section - Shows when flowId exists */}
369
- {flowId && (
370
- <>
371
- <Text style={styles.inputLabel}>Verification code</Text>
372
- <TextInput
373
- style={styles.input}
374
- value={otp}
375
- onChangeText={setOtp}
376
- placeholder="Enter 6-digit code"
377
- keyboardType="number-pad"
378
- maxLength={6}
379
- editable={!isLoading}
380
- placeholderTextColor={colors.textSecondary}
381
- returnKeyType="done"
382
- onSubmitEditing={onVerifyOTP}
383
- blurOnSubmit={true}
384
- autoFocus={true}
385
- />
386
- </>
387
- )}
388
-
389
- {/* Action Button */}
390
- <TouchableOpacity
391
- style={[styles.continueButton, isLoading && styles.buttonDisabled]}
392
- onPress={flowId ? onVerifyOTP : onSignIn}
393
- disabled={isLoading}
394
- >
395
- <Text style={styles.continueButtonText}>
396
- {isLoading
397
- ? flowId
398
- ? "Verifying..."
399
- : "Sending..."
400
- : flowId
401
- ? "Verify Code"
402
- : "Continue"}
403
- </Text>
404
- </TouchableOpacity>
405
-
406
- {/* Auth Method Toggle - Only show when not in OTP mode */}
407
- {!flowId && (
408
- <>
409
- <View style={styles.dividerContainer}>
410
- <View style={styles.dividerLine} />
411
- <Text style={styles.dividerText}>OR</Text>
412
- <View style={styles.dividerLine} />
413
- </View>
414
-
415
- <TouchableOpacity
416
- style={styles.toggleButton}
417
- onPress={onAuthMethodToggle}
418
- disabled={isLoading}
419
- >
420
- <Text style={styles.toggleIcon}>{authMethod === "email" ? "πŸ“ž" : "βœ‰οΈ"}</Text>
421
- <Text style={styles.toggleButtonText}>
422
- Continue with {authMethod === "email" ? "phone" : "email"}
423
- </Text>
424
- </TouchableOpacity>
425
- </>
426
- )}
186
+ <View style={styles.toggleButtonContainer}>
187
+ {authMethod !== "email" && (
188
+ <TouchableOpacity
189
+ style={styles.toggleButton}
190
+ onPress={() => setAuthMethod("email")}
191
+ >
192
+ <Text style={styles.toggleIcon}>{"βœ‰οΈ"}</Text>
193
+ <Text style={styles.toggleButtonText}>Continue with Email</Text>
194
+ </TouchableOpacity>
195
+ )}
196
+ {authMethod !== "sms" && (
197
+ <TouchableOpacity
198
+ style={styles.toggleButton}
199
+ onPress={() => setAuthMethod("sms")}
200
+ >
201
+ <Text style={styles.toggleIcon}>{"πŸ“ž"}</Text>
202
+ <Text style={styles.toggleButtonText}>Continue with SMS</Text>
203
+ </TouchableOpacity>
204
+ )}
205
+ {authMethod !== "oauth" && (
206
+ <TouchableOpacity
207
+ style={styles.toggleButton}
208
+ onPress={() => setAuthMethod("oauth")}
209
+ >
210
+ <Text style={styles.toggleButtonText}>Continue with Social</Text>
211
+ </TouchableOpacity>
212
+ )}
213
+ </View>
214
+ </View>
427
215
  </View>
428
- </View>
429
- </View>
430
- </ScrollView>
431
- </KeyboardAvoidingView>
216
+ </ScrollView>
217
+ </KeyboardAvoidingView>
218
+ </View>
219
+ </View>
432
220
  );
433
221
  };
@@ -0,0 +1,80 @@
1
+ import React, { useState } from "react";
2
+ import { View, TouchableOpacity, Text, TextInput, Alert } from "react-native";
3
+ import { useSignInWithSms } from "@coinbase/cdp-hooks";
4
+ import { useTheme } from "../theme/ThemeContext";
5
+ import { useSignInFormStyles } from "../hooks/useSignInFormStyles";
6
+
7
+ export interface SmsFormProps {
8
+ onSuccess: (flowId: string) => void;
9
+ }
10
+
11
+ export const SmsForm: React.FC<SmsFormProps> = ({ onSuccess }) => {
12
+ const { colors } = useTheme();
13
+ const { signInWithSms } = useSignInWithSms();
14
+ const styles = useSignInFormStyles();
15
+ const [phoneNumber, setPhoneNumber] = useState("");
16
+ const [isLoading, setIsLoading] = useState(false);
17
+
18
+ const handleSignIn = async () => {
19
+ if (!phoneNumber) {
20
+ Alert.alert("Error", "Please enter a phone number.");
21
+ return;
22
+ }
23
+
24
+ setIsLoading(true);
25
+ try {
26
+ // Format phone number with country code for SMS
27
+ const formattedPhoneNumber = phoneNumber.startsWith("+")
28
+ ? phoneNumber
29
+ : `+1${phoneNumber.replace(/\D/g, "")}`;
30
+ const result = await signInWithSms({ phoneNumber: formattedPhoneNumber });
31
+ onSuccess(result.flowId);
32
+ } catch (error) {
33
+ Alert.alert("Error", error instanceof Error ? error.message : "Failed to sign in.");
34
+ } finally {
35
+ setIsLoading(false);
36
+ }
37
+ };
38
+
39
+ return (
40
+ <>
41
+ <View style={styles.header}>
42
+ <View style={styles.logoCircle}>
43
+ <Text style={styles.logoText}>C</Text>
44
+ </View>
45
+ <Text style={styles.title}>Sign in</Text>
46
+ </View>
47
+ <View style={styles.form}>
48
+ <Text style={styles.inputLabel}>Phone number</Text>
49
+ <View style={styles.inputContainer}>
50
+ <View style={styles.flagContainer}>
51
+ <Text style={styles.flagText}>πŸ‡ΊπŸ‡Έ</Text>
52
+ <Text style={styles.countryCode}>+1</Text>
53
+ </View>
54
+ <TextInput
55
+ style={styles.phoneInput}
56
+ value={phoneNumber}
57
+ onChangeText={setPhoneNumber}
58
+ placeholder="(000) 000-0000"
59
+ keyboardType="phone-pad"
60
+ autoCapitalize="none"
61
+ autoCorrect={false}
62
+ editable={!isLoading}
63
+ placeholderTextColor={colors.textSecondary}
64
+ returnKeyType="done"
65
+ onSubmitEditing={handleSignIn}
66
+ blurOnSubmit={true}
67
+ />
68
+ </View>
69
+
70
+ <TouchableOpacity
71
+ style={[styles.continueButton, isLoading && styles.buttonDisabled]}
72
+ onPress={handleSignIn}
73
+ disabled={isLoading}
74
+ >
75
+ <Text style={styles.continueButtonText}>{isLoading ? "Sending..." : "Continue"}</Text>
76
+ </TouchableOpacity>
77
+ </View>
78
+ </>
79
+ );
80
+ };
@@ -4,3 +4,4 @@ EXPO_PUBLIC_CDP_CREATE_ETHEREUM_ACCOUNT_TYPE=smart
4
4
  EXPO_PUBLIC_CDP_CREATE_SOLANA_ACCOUNT=false
5
5
  EXPO_PUBLIC_CDP_BASE_PATH=https://api.cdp.coinbase.com/platform
6
6
  EXPO_PUBLIC_USE_MOCK=false
7
+ EXPO_PUBLIC_NATIVE_OAUTH_CALLBACK=cdp-rn-demo://callback