@supatype/react-native-auth 0.1.6
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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +14 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/README.md +66 -0
- package/dist/AuthGate.d.ts +15 -0
- package/dist/AuthGate.d.ts.map +1 -0
- package/dist/AuthGate.js +19 -0
- package/dist/AuthGate.js.map +1 -0
- package/dist/LoginForm.d.ts +19 -0
- package/dist/LoginForm.d.ts.map +1 -0
- package/dist/LoginForm.js +45 -0
- package/dist/LoginForm.js.map +1 -0
- package/dist/MagicLinkForm.d.ts +20 -0
- package/dist/MagicLinkForm.d.ts.map +1 -0
- package/dist/MagicLinkForm.js +50 -0
- package/dist/MagicLinkForm.js.map +1 -0
- package/dist/OAuthButton.d.ts +27 -0
- package/dist/OAuthButton.d.ts.map +1 -0
- package/dist/OAuthButton.js +54 -0
- package/dist/OAuthButton.js.map +1 -0
- package/dist/ResetPassword.d.ts +28 -0
- package/dist/ResetPassword.d.ts.map +1 -0
- package/dist/ResetPassword.js +73 -0
- package/dist/ResetPassword.js.map +1 -0
- package/dist/SignUpForm.d.ts +20 -0
- package/dist/SignUpForm.d.ts.map +1 -0
- package/dist/SignUpForm.js +55 -0
- package/dist/SignUpForm.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.d.ts +76 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +77 -0
- package/dist/styles.js.map +1 -0
- package/dist/theme.d.ts +22 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +23 -0
- package/dist/theme.js.map +1 -0
- package/package.json +53 -0
- package/src/AuthGate.tsx +43 -0
- package/src/LoginForm.tsx +142 -0
- package/src/MagicLinkForm.tsx +136 -0
- package/src/OAuthButton.tsx +99 -0
- package/src/ResetPassword.tsx +191 -0
- package/src/SignUpForm.tsx +160 -0
- package/src/index.ts +24 -0
- package/src/react-native.d.ts +103 -0
- package/src/styles.ts +78 -0
- package/src/theme.tsx +45 -0
- package/tests/auth-ui.test.tsx +186 -0
- package/tests/mocks/react-native.ts +23 -0
- package/tsconfig.json +17 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { useState, type ReactNode } from "react"
|
|
2
|
+
import { Pressable, Text, type StyleProp, type ViewStyle } from "react-native"
|
|
3
|
+
import type { Session, SupatypeError } from "@supatype/client"
|
|
4
|
+
import { useSupatype } from "@supatype/react"
|
|
5
|
+
import { openOAuth, type WebBrowserLike } from "@supatype/react-native"
|
|
6
|
+
import { useAuthTheme } from "./theme.js"
|
|
7
|
+
import { fieldStyles } from "./styles.js"
|
|
8
|
+
|
|
9
|
+
export interface OAuthButtonProps {
|
|
10
|
+
provider: string
|
|
11
|
+
/** Deep link URI (required for mobile OAuth). */
|
|
12
|
+
redirectTo: string
|
|
13
|
+
/** Pass `import * as WebBrowser from "expo-web-browser"`. */
|
|
14
|
+
webBrowser: WebBrowserLike
|
|
15
|
+
scopes?: string | undefined
|
|
16
|
+
onSuccess?: ((session: Session) => void) | undefined
|
|
17
|
+
onError?: ((error: { message: string }) => void) | undefined
|
|
18
|
+
onCancel?: (() => void) | undefined
|
|
19
|
+
style?: StyleProp<ViewStyle> | undefined
|
|
20
|
+
children?: ReactNode | undefined
|
|
21
|
+
disabled?: boolean | undefined
|
|
22
|
+
label?: string | undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function capitalize(s: string): string {
|
|
26
|
+
return s.charAt(0).toUpperCase() + s.slice(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* OAuth sign-in button. Uses PKCE via `@supatype/react-native` `openOAuth`.
|
|
31
|
+
*/
|
|
32
|
+
export function OAuthButton({
|
|
33
|
+
provider,
|
|
34
|
+
redirectTo,
|
|
35
|
+
webBrowser,
|
|
36
|
+
scopes,
|
|
37
|
+
onSuccess,
|
|
38
|
+
onError,
|
|
39
|
+
onCancel,
|
|
40
|
+
style,
|
|
41
|
+
children,
|
|
42
|
+
disabled,
|
|
43
|
+
label,
|
|
44
|
+
}: OAuthButtonProps): React.ReactElement {
|
|
45
|
+
const client = useSupatype()
|
|
46
|
+
const theme = useAuthTheme()
|
|
47
|
+
const styles = fieldStyles(theme)
|
|
48
|
+
const [loading, setLoading] = useState(false)
|
|
49
|
+
const providerName = capitalize(provider)
|
|
50
|
+
const isDisabled = disabled === true || loading
|
|
51
|
+
|
|
52
|
+
async function handlePress(): Promise<void> {
|
|
53
|
+
setLoading(true)
|
|
54
|
+
try {
|
|
55
|
+
const { data, error, cancelled } = await openOAuth(client, {
|
|
56
|
+
provider,
|
|
57
|
+
redirectTo,
|
|
58
|
+
webBrowser,
|
|
59
|
+
...(scopes !== undefined && { scopes }),
|
|
60
|
+
})
|
|
61
|
+
if (cancelled) {
|
|
62
|
+
onCancel?.()
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
if (error !== null) {
|
|
66
|
+
onError?.(error)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
if (data.session !== null) {
|
|
70
|
+
onSuccess?.(data.session)
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
onError?.({ message: err instanceof Error ? err.message : "OAuth failed" })
|
|
74
|
+
} finally {
|
|
75
|
+
setLoading(false)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<Pressable
|
|
81
|
+
testID={`st-oauth-${provider}`}
|
|
82
|
+
accessibilityRole="button"
|
|
83
|
+
accessibilityLabel={label ?? `Sign in with ${providerName}`}
|
|
84
|
+
accessibilityState={{ disabled: isDisabled }}
|
|
85
|
+
disabled={isDisabled}
|
|
86
|
+
onPress={() => {
|
|
87
|
+
void handlePress()
|
|
88
|
+
}}
|
|
89
|
+
style={[styles.secondaryButton, isDisabled ? styles.buttonDisabled : null, style]}
|
|
90
|
+
>
|
|
91
|
+
<Text style={styles.secondaryButtonText}>
|
|
92
|
+
{children ??
|
|
93
|
+
(loading ? `Connecting to ${providerName}…` : (label ?? `Sign in with ${providerName}`))}
|
|
94
|
+
</Text>
|
|
95
|
+
</Pressable>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type { SupatypeError }
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import React, { useState } from "react"
|
|
2
|
+
import {
|
|
3
|
+
KeyboardAvoidingView,
|
|
4
|
+
Platform,
|
|
5
|
+
Pressable,
|
|
6
|
+
ScrollView,
|
|
7
|
+
Text,
|
|
8
|
+
TextInput,
|
|
9
|
+
View,
|
|
10
|
+
type StyleProp,
|
|
11
|
+
type ViewStyle,
|
|
12
|
+
} from "react-native"
|
|
13
|
+
import type { Session, SupatypeError } from "@supatype/client"
|
|
14
|
+
import { useSupatype } from "@supatype/react"
|
|
15
|
+
import { useAuthTheme } from "./theme.js"
|
|
16
|
+
import { fieldStyles } from "./styles.js"
|
|
17
|
+
|
|
18
|
+
export interface ResetPasswordLabels {
|
|
19
|
+
requestTitle?: string | undefined
|
|
20
|
+
email?: string | undefined
|
|
21
|
+
requestSubmit?: string | undefined
|
|
22
|
+
requestSuccess?: string | undefined
|
|
23
|
+
updateTitle?: string | undefined
|
|
24
|
+
password?: string | undefined
|
|
25
|
+
updateSubmit?: string | undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ResetPasswordProps {
|
|
29
|
+
/**
|
|
30
|
+
* When set (from a recovery deep link after the user is signed in via recovery),
|
|
31
|
+
* shows the new-password form and calls `updateUser({ password })`.
|
|
32
|
+
*/
|
|
33
|
+
mode?: "request" | "update" | undefined
|
|
34
|
+
redirectTo?: string | undefined
|
|
35
|
+
onRequestSent?: (() => void) | undefined
|
|
36
|
+
onPasswordUpdated?: ((session: Session | null) => void) | undefined
|
|
37
|
+
onError?: ((error: SupatypeError) => void) | undefined
|
|
38
|
+
labels?: ResetPasswordLabels | undefined
|
|
39
|
+
style?: StyleProp<ViewStyle> | undefined
|
|
40
|
+
contentContainerStyle?: StyleProp<ViewStyle> | undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DEFAULT_LABELS: Required<ResetPasswordLabels> = {
|
|
44
|
+
requestTitle: "Reset password",
|
|
45
|
+
email: "Email address",
|
|
46
|
+
requestSubmit: "Send reset link",
|
|
47
|
+
requestSuccess: "Check your email for a password reset link.",
|
|
48
|
+
updateTitle: "Choose a new password",
|
|
49
|
+
password: "New password",
|
|
50
|
+
updateSubmit: "Update password",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function ResetPassword({
|
|
54
|
+
mode = "request",
|
|
55
|
+
redirectTo,
|
|
56
|
+
onRequestSent,
|
|
57
|
+
onPasswordUpdated,
|
|
58
|
+
onError,
|
|
59
|
+
labels,
|
|
60
|
+
style,
|
|
61
|
+
contentContainerStyle,
|
|
62
|
+
}: ResetPasswordProps): React.ReactElement {
|
|
63
|
+
const client = useSupatype()
|
|
64
|
+
const theme = useAuthTheme()
|
|
65
|
+
const styles = fieldStyles(theme)
|
|
66
|
+
const l = { ...DEFAULT_LABELS, ...labels }
|
|
67
|
+
|
|
68
|
+
const [email, setEmail] = useState("")
|
|
69
|
+
const [password, setPassword] = useState("")
|
|
70
|
+
const [loading, setLoading] = useState(false)
|
|
71
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
|
72
|
+
const [sent, setSent] = useState(false)
|
|
73
|
+
|
|
74
|
+
async function handleRequest(): Promise<void> {
|
|
75
|
+
setErrorMessage(null)
|
|
76
|
+
setLoading(true)
|
|
77
|
+
try {
|
|
78
|
+
const { error } = await client.auth.resetPasswordForEmail(
|
|
79
|
+
email,
|
|
80
|
+
redirectTo !== undefined ? { redirectTo } : undefined,
|
|
81
|
+
)
|
|
82
|
+
if (error !== null) {
|
|
83
|
+
setErrorMessage(error.message)
|
|
84
|
+
onError?.(error)
|
|
85
|
+
} else {
|
|
86
|
+
setSent(true)
|
|
87
|
+
onRequestSent?.()
|
|
88
|
+
}
|
|
89
|
+
} finally {
|
|
90
|
+
setLoading(false)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function handleUpdate(): Promise<void> {
|
|
95
|
+
setErrorMessage(null)
|
|
96
|
+
setLoading(true)
|
|
97
|
+
try {
|
|
98
|
+
const { error } = await client.auth.updateUser({ password })
|
|
99
|
+
if (error !== null) {
|
|
100
|
+
setErrorMessage(error.message)
|
|
101
|
+
onError?.(error)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
const { data } = await client.auth.getSession()
|
|
105
|
+
onPasswordUpdated?.(data.session)
|
|
106
|
+
} finally {
|
|
107
|
+
setLoading(false)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (mode === "request" && sent) {
|
|
112
|
+
return (
|
|
113
|
+
<View style={[styles.root, style]} testID="st-reset-sent">
|
|
114
|
+
<Text style={styles.muted} accessibilityLiveRegion="polite">
|
|
115
|
+
{l.requestSuccess}
|
|
116
|
+
</Text>
|
|
117
|
+
</View>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const isUpdate = mode === "update"
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<KeyboardAvoidingView
|
|
125
|
+
style={[{ flex: 1 }, style]}
|
|
126
|
+
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
|
127
|
+
>
|
|
128
|
+
<ScrollView
|
|
129
|
+
keyboardShouldPersistTaps="handled"
|
|
130
|
+
contentContainerStyle={[styles.root, contentContainerStyle]}
|
|
131
|
+
>
|
|
132
|
+
<Text style={styles.title}>{isUpdate ? l.updateTitle : l.requestTitle}</Text>
|
|
133
|
+
|
|
134
|
+
{errorMessage !== null && (
|
|
135
|
+
<Text style={styles.error} accessibilityRole="alert">
|
|
136
|
+
{errorMessage}
|
|
137
|
+
</Text>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{!isUpdate ? (
|
|
141
|
+
<View>
|
|
142
|
+
<Text style={styles.label}>{l.email}</Text>
|
|
143
|
+
<TextInput
|
|
144
|
+
testID="st-reset-email"
|
|
145
|
+
style={styles.input}
|
|
146
|
+
value={email}
|
|
147
|
+
onChangeText={setEmail}
|
|
148
|
+
autoCapitalize="none"
|
|
149
|
+
keyboardType="email-address"
|
|
150
|
+
editable={!loading}
|
|
151
|
+
accessibilityLabel={l.email}
|
|
152
|
+
placeholderTextColor={theme.colorTextMuted}
|
|
153
|
+
/>
|
|
154
|
+
</View>
|
|
155
|
+
) : (
|
|
156
|
+
<View>
|
|
157
|
+
<Text style={styles.label}>{l.password}</Text>
|
|
158
|
+
<TextInput
|
|
159
|
+
testID="st-reset-password"
|
|
160
|
+
style={styles.input}
|
|
161
|
+
value={password}
|
|
162
|
+
onChangeText={setPassword}
|
|
163
|
+
secureTextEntry
|
|
164
|
+
editable={!loading}
|
|
165
|
+
accessibilityLabel={l.password}
|
|
166
|
+
placeholderTextColor={theme.colorTextMuted}
|
|
167
|
+
/>
|
|
168
|
+
</View>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
<Pressable
|
|
172
|
+
testID="st-reset-submit"
|
|
173
|
+
accessibilityRole="button"
|
|
174
|
+
disabled={loading}
|
|
175
|
+
onPress={() => {
|
|
176
|
+
void (isUpdate ? handleUpdate() : handleRequest())
|
|
177
|
+
}}
|
|
178
|
+
style={[styles.button, loading ? styles.buttonDisabled : null]}
|
|
179
|
+
>
|
|
180
|
+
<Text style={styles.buttonText}>
|
|
181
|
+
{loading
|
|
182
|
+
? "Please wait…"
|
|
183
|
+
: isUpdate
|
|
184
|
+
? l.updateSubmit
|
|
185
|
+
: l.requestSubmit}
|
|
186
|
+
</Text>
|
|
187
|
+
</Pressable>
|
|
188
|
+
</ScrollView>
|
|
189
|
+
</KeyboardAvoidingView>
|
|
190
|
+
)
|
|
191
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import React, { useState } from "react"
|
|
2
|
+
import {
|
|
3
|
+
KeyboardAvoidingView,
|
|
4
|
+
Platform,
|
|
5
|
+
Pressable,
|
|
6
|
+
ScrollView,
|
|
7
|
+
Text,
|
|
8
|
+
TextInput,
|
|
9
|
+
View,
|
|
10
|
+
type StyleProp,
|
|
11
|
+
type ViewStyle,
|
|
12
|
+
} from "react-native"
|
|
13
|
+
import type { Session, SupatypeError } from "@supatype/client"
|
|
14
|
+
import { useAuth } from "@supatype/react"
|
|
15
|
+
import { useAuthTheme } from "./theme.js"
|
|
16
|
+
import { fieldStyles } from "./styles.js"
|
|
17
|
+
|
|
18
|
+
export interface SignUpFormLabels {
|
|
19
|
+
title?: string | undefined
|
|
20
|
+
email?: string | undefined
|
|
21
|
+
password?: string | undefined
|
|
22
|
+
submit?: string | undefined
|
|
23
|
+
successMessage?: string | undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SignUpFormProps {
|
|
27
|
+
onSuccess?: ((session: Session | null) => void) | undefined
|
|
28
|
+
onError?: ((error: SupatypeError) => void) | undefined
|
|
29
|
+
labels?: SignUpFormLabels | undefined
|
|
30
|
+
metadata?: Record<string, unknown> | undefined
|
|
31
|
+
style?: StyleProp<ViewStyle> | undefined
|
|
32
|
+
contentContainerStyle?: StyleProp<ViewStyle> | undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_LABELS: Required<SignUpFormLabels> = {
|
|
36
|
+
title: "Create account",
|
|
37
|
+
email: "Email address",
|
|
38
|
+
password: "Password",
|
|
39
|
+
submit: "Create account",
|
|
40
|
+
successMessage: "Check your email to confirm your account.",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function SignUpForm({
|
|
44
|
+
onSuccess,
|
|
45
|
+
onError,
|
|
46
|
+
labels,
|
|
47
|
+
metadata,
|
|
48
|
+
style,
|
|
49
|
+
contentContainerStyle,
|
|
50
|
+
}: SignUpFormProps): React.ReactElement {
|
|
51
|
+
const { signUp } = useAuth()
|
|
52
|
+
const theme = useAuthTheme()
|
|
53
|
+
const styles = fieldStyles(theme)
|
|
54
|
+
const l = { ...DEFAULT_LABELS, ...labels }
|
|
55
|
+
|
|
56
|
+
const [email, setEmail] = useState("")
|
|
57
|
+
const [password, setPassword] = useState("")
|
|
58
|
+
const [loading, setLoading] = useState(false)
|
|
59
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
|
60
|
+
const [confirmed, setConfirmed] = useState(false)
|
|
61
|
+
|
|
62
|
+
async function handleSubmit(): Promise<void> {
|
|
63
|
+
setErrorMessage(null)
|
|
64
|
+
setLoading(true)
|
|
65
|
+
try {
|
|
66
|
+
const { data, error } = await signUp({
|
|
67
|
+
email,
|
|
68
|
+
password,
|
|
69
|
+
...(metadata !== undefined && { options: { data: metadata } }),
|
|
70
|
+
})
|
|
71
|
+
if (error !== null) {
|
|
72
|
+
setErrorMessage(error.message)
|
|
73
|
+
onError?.(error)
|
|
74
|
+
} else {
|
|
75
|
+
if (data.session === null) {
|
|
76
|
+
setConfirmed(true)
|
|
77
|
+
}
|
|
78
|
+
onSuccess?.(data.session)
|
|
79
|
+
}
|
|
80
|
+
} finally {
|
|
81
|
+
setLoading(false)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (confirmed) {
|
|
86
|
+
return (
|
|
87
|
+
<View style={[styles.root, style]}>
|
|
88
|
+
<Text style={styles.muted} accessibilityRole="text" accessibilityLiveRegion="polite">
|
|
89
|
+
{l.successMessage}
|
|
90
|
+
</Text>
|
|
91
|
+
</View>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<KeyboardAvoidingView
|
|
97
|
+
style={[{ flex: 1 }, style]}
|
|
98
|
+
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
|
99
|
+
>
|
|
100
|
+
<ScrollView
|
|
101
|
+
keyboardShouldPersistTaps="handled"
|
|
102
|
+
contentContainerStyle={[styles.root, contentContainerStyle]}
|
|
103
|
+
>
|
|
104
|
+
<Text style={styles.title}>{l.title}</Text>
|
|
105
|
+
|
|
106
|
+
{errorMessage !== null && (
|
|
107
|
+
<Text style={styles.error} accessibilityRole="alert" accessibilityLiveRegion="polite">
|
|
108
|
+
{errorMessage}
|
|
109
|
+
</Text>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
<View>
|
|
113
|
+
<Text style={styles.label}>{l.email}</Text>
|
|
114
|
+
<TextInput
|
|
115
|
+
testID="st-signup-email"
|
|
116
|
+
style={styles.input}
|
|
117
|
+
value={email}
|
|
118
|
+
onChangeText={setEmail}
|
|
119
|
+
autoCapitalize="none"
|
|
120
|
+
keyboardType="email-address"
|
|
121
|
+
autoComplete="email"
|
|
122
|
+
textContentType="emailAddress"
|
|
123
|
+
editable={!loading}
|
|
124
|
+
accessibilityLabel={l.email}
|
|
125
|
+
placeholderTextColor={theme.colorTextMuted}
|
|
126
|
+
/>
|
|
127
|
+
</View>
|
|
128
|
+
|
|
129
|
+
<View>
|
|
130
|
+
<Text style={styles.label}>{l.password}</Text>
|
|
131
|
+
<TextInput
|
|
132
|
+
testID="st-signup-password"
|
|
133
|
+
style={styles.input}
|
|
134
|
+
value={password}
|
|
135
|
+
onChangeText={setPassword}
|
|
136
|
+
secureTextEntry
|
|
137
|
+
autoComplete="password-new"
|
|
138
|
+
textContentType="newPassword"
|
|
139
|
+
editable={!loading}
|
|
140
|
+
accessibilityLabel={l.password}
|
|
141
|
+
placeholderTextColor={theme.colorTextMuted}
|
|
142
|
+
/>
|
|
143
|
+
</View>
|
|
144
|
+
|
|
145
|
+
<Pressable
|
|
146
|
+
testID="st-signup-submit"
|
|
147
|
+
accessibilityRole="button"
|
|
148
|
+
accessibilityLabel={l.submit}
|
|
149
|
+
disabled={loading}
|
|
150
|
+
onPress={() => {
|
|
151
|
+
void handleSubmit()
|
|
152
|
+
}}
|
|
153
|
+
style={[styles.button, loading ? styles.buttonDisabled : null]}
|
|
154
|
+
>
|
|
155
|
+
<Text style={styles.buttonText}>{loading ? "Creating account…" : l.submit}</Text>
|
|
156
|
+
</Pressable>
|
|
157
|
+
</ScrollView>
|
|
158
|
+
</KeyboardAvoidingView>
|
|
159
|
+
)
|
|
160
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { LoginForm } from "./LoginForm.js"
|
|
2
|
+
export type { LoginFormProps, LoginFormLabels } from "./LoginForm.js"
|
|
3
|
+
|
|
4
|
+
export { SignUpForm } from "./SignUpForm.js"
|
|
5
|
+
export type { SignUpFormProps, SignUpFormLabels } from "./SignUpForm.js"
|
|
6
|
+
|
|
7
|
+
export { MagicLinkForm } from "./MagicLinkForm.js"
|
|
8
|
+
export type { MagicLinkFormProps, MagicLinkFormLabels } from "./MagicLinkForm.js"
|
|
9
|
+
|
|
10
|
+
export { OAuthButton } from "./OAuthButton.js"
|
|
11
|
+
export type { OAuthButtonProps } from "./OAuthButton.js"
|
|
12
|
+
|
|
13
|
+
export { AuthGate } from "./AuthGate.js"
|
|
14
|
+
export type { AuthGateProps } from "./AuthGate.js"
|
|
15
|
+
|
|
16
|
+
export { ResetPassword } from "./ResetPassword.js"
|
|
17
|
+
export type { ResetPasswordProps, ResetPasswordLabels } from "./ResetPassword.js"
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
AuthThemeProvider,
|
|
21
|
+
useAuthTheme,
|
|
22
|
+
defaultAuthTheme,
|
|
23
|
+
} from "./theme.js"
|
|
24
|
+
export type { AuthThemeTokens, AuthThemeProviderProps } from "./theme.js"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
declare module "react-native" {
|
|
2
|
+
import type * as React from "react"
|
|
3
|
+
|
|
4
|
+
export type StyleProp<T> =
|
|
5
|
+
| T
|
|
6
|
+
| Array<T | StyleProp<T> | null | undefined | false>
|
|
7
|
+
| null
|
|
8
|
+
| undefined
|
|
9
|
+
| false
|
|
10
|
+
|
|
11
|
+
export interface ViewStyle {
|
|
12
|
+
flex?: number | undefined
|
|
13
|
+
padding?: number | undefined
|
|
14
|
+
paddingVertical?: number | undefined
|
|
15
|
+
paddingHorizontal?: number | undefined
|
|
16
|
+
marginBottom?: number | undefined
|
|
17
|
+
marginTop?: number | undefined
|
|
18
|
+
gap?: number | undefined
|
|
19
|
+
borderWidth?: number | undefined
|
|
20
|
+
borderColor?: string | undefined
|
|
21
|
+
borderRadius?: number | undefined
|
|
22
|
+
backgroundColor?: string | undefined
|
|
23
|
+
alignItems?: "flex-start" | "flex-end" | "center" | "stretch" | undefined
|
|
24
|
+
justifyContent?: "flex-start" | "flex-end" | "center" | "space-between" | undefined
|
|
25
|
+
opacity?: number | undefined
|
|
26
|
+
width?: number | string | undefined
|
|
27
|
+
minHeight?: number | undefined
|
|
28
|
+
flexDirection?: "row" | "column" | undefined
|
|
29
|
+
}
|
|
30
|
+
export interface TextStyle {
|
|
31
|
+
color?: string | undefined
|
|
32
|
+
fontSize?: number | undefined
|
|
33
|
+
fontWeight?: "400" | "500" | "600" | "700" | "normal" | "bold" | undefined
|
|
34
|
+
marginBottom?: number | undefined
|
|
35
|
+
textAlign?: "auto" | "left" | "right" | "center" | undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ViewProps {
|
|
39
|
+
style?: StyleProp<ViewStyle> | undefined
|
|
40
|
+
children?: React.ReactNode | undefined
|
|
41
|
+
accessibilityRole?: string | undefined
|
|
42
|
+
accessibilityLiveRegion?: "none" | "polite" | "assertive" | undefined
|
|
43
|
+
testID?: string | undefined
|
|
44
|
+
}
|
|
45
|
+
export interface TextProps {
|
|
46
|
+
style?: StyleProp<TextStyle> | undefined
|
|
47
|
+
children?: React.ReactNode | undefined
|
|
48
|
+
accessibilityRole?: string | undefined
|
|
49
|
+
accessibilityLiveRegion?: "none" | "polite" | "assertive" | undefined
|
|
50
|
+
}
|
|
51
|
+
export interface TextInputProps {
|
|
52
|
+
style?: StyleProp<TextStyle | ViewStyle> | undefined
|
|
53
|
+
value?: string | undefined
|
|
54
|
+
onChangeText?: ((text: string) => void) | undefined
|
|
55
|
+
placeholder?: string | undefined
|
|
56
|
+
placeholderTextColor?: string | undefined
|
|
57
|
+
secureTextEntry?: boolean | undefined
|
|
58
|
+
autoCapitalize?: "none" | "sentences" | "words" | "characters" | undefined
|
|
59
|
+
autoComplete?: string | undefined
|
|
60
|
+
keyboardType?: "default" | "email-address" | "numeric" | undefined
|
|
61
|
+
textContentType?: string | undefined
|
|
62
|
+
editable?: boolean | undefined
|
|
63
|
+
accessibilityLabel?: string | undefined
|
|
64
|
+
testID?: string | undefined
|
|
65
|
+
}
|
|
66
|
+
export interface PressableProps {
|
|
67
|
+
style?: StyleProp<ViewStyle> | ((state: { pressed: boolean }) => StyleProp<ViewStyle>) | undefined
|
|
68
|
+
onPress?: (() => void) | undefined
|
|
69
|
+
disabled?: boolean | undefined
|
|
70
|
+
accessibilityRole?: string | undefined
|
|
71
|
+
accessibilityLabel?: string | undefined
|
|
72
|
+
accessibilityState?: { disabled?: boolean | undefined } | undefined
|
|
73
|
+
children?: React.ReactNode | undefined
|
|
74
|
+
testID?: string | undefined
|
|
75
|
+
}
|
|
76
|
+
export interface ScrollViewProps {
|
|
77
|
+
style?: StyleProp<ViewStyle> | undefined
|
|
78
|
+
contentContainerStyle?: StyleProp<ViewStyle> | undefined
|
|
79
|
+
keyboardShouldPersistTaps?: "always" | "never" | "handled" | undefined
|
|
80
|
+
children?: React.ReactNode | undefined
|
|
81
|
+
}
|
|
82
|
+
export interface KeyboardAvoidingViewProps {
|
|
83
|
+
style?: StyleProp<ViewStyle> | undefined
|
|
84
|
+
behavior?: "height" | "position" | "padding" | undefined
|
|
85
|
+
children?: React.ReactNode | undefined
|
|
86
|
+
}
|
|
87
|
+
export interface ActivityIndicatorProps {
|
|
88
|
+
color?: string | undefined
|
|
89
|
+
testID?: string | undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const View: React.ComponentType<ViewProps>
|
|
93
|
+
export const Text: React.ComponentType<TextProps>
|
|
94
|
+
export const TextInput: React.ComponentType<TextInputProps>
|
|
95
|
+
export const Pressable: React.ComponentType<PressableProps>
|
|
96
|
+
export const ScrollView: React.ComponentType<ScrollViewProps>
|
|
97
|
+
export const KeyboardAvoidingView: React.ComponentType<KeyboardAvoidingViewProps>
|
|
98
|
+
export const ActivityIndicator: React.ComponentType<ActivityIndicatorProps>
|
|
99
|
+
export const Platform: { OS: "ios" | "android" | "web" | "windows" | "macos" }
|
|
100
|
+
export const StyleSheet: {
|
|
101
|
+
create<T extends Record<string, ViewStyle | TextStyle>>(styles: T): T
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/styles.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { AuthThemeTokens } from "./theme.js"
|
|
2
|
+
|
|
3
|
+
export function fieldStyles(theme: AuthThemeTokens) {
|
|
4
|
+
return {
|
|
5
|
+
root: {
|
|
6
|
+
backgroundColor: theme.colorBackground,
|
|
7
|
+
padding: theme.spacing,
|
|
8
|
+
gap: theme.spacing,
|
|
9
|
+
},
|
|
10
|
+
title: {
|
|
11
|
+
color: theme.colorText,
|
|
12
|
+
fontSize: theme.fontSizeTitle,
|
|
13
|
+
fontWeight: "600" as const,
|
|
14
|
+
marginBottom: theme.spacing / 2,
|
|
15
|
+
},
|
|
16
|
+
label: {
|
|
17
|
+
color: theme.colorText,
|
|
18
|
+
fontSize: theme.fontSize,
|
|
19
|
+
marginBottom: 6,
|
|
20
|
+
fontWeight: "500" as const,
|
|
21
|
+
},
|
|
22
|
+
input: {
|
|
23
|
+
borderWidth: 1,
|
|
24
|
+
borderColor: theme.colorBorder,
|
|
25
|
+
borderRadius: theme.borderRadius,
|
|
26
|
+
paddingHorizontal: 12,
|
|
27
|
+
paddingVertical: 10,
|
|
28
|
+
fontSize: theme.fontSize,
|
|
29
|
+
color: theme.colorText,
|
|
30
|
+
backgroundColor: theme.colorSurface,
|
|
31
|
+
minHeight: 44,
|
|
32
|
+
},
|
|
33
|
+
error: {
|
|
34
|
+
color: theme.colorDanger,
|
|
35
|
+
fontSize: theme.fontSize,
|
|
36
|
+
marginBottom: 4,
|
|
37
|
+
},
|
|
38
|
+
muted: {
|
|
39
|
+
color: theme.colorTextMuted,
|
|
40
|
+
fontSize: theme.fontSize,
|
|
41
|
+
},
|
|
42
|
+
button: {
|
|
43
|
+
backgroundColor: theme.colorPrimary,
|
|
44
|
+
borderRadius: theme.borderRadius,
|
|
45
|
+
paddingVertical: 12,
|
|
46
|
+
paddingHorizontal: 16,
|
|
47
|
+
alignItems: "center" as const,
|
|
48
|
+
justifyContent: "center" as const,
|
|
49
|
+
minHeight: 44,
|
|
50
|
+
},
|
|
51
|
+
buttonDisabled: {
|
|
52
|
+
opacity: 0.6,
|
|
53
|
+
},
|
|
54
|
+
buttonText: {
|
|
55
|
+
color: "#ffffff",
|
|
56
|
+
fontSize: theme.fontSize,
|
|
57
|
+
fontWeight: "600" as const,
|
|
58
|
+
},
|
|
59
|
+
secondaryButton: {
|
|
60
|
+
backgroundColor: theme.colorSurface,
|
|
61
|
+
borderWidth: 1,
|
|
62
|
+
borderColor: theme.colorBorder,
|
|
63
|
+
borderRadius: theme.borderRadius,
|
|
64
|
+
paddingVertical: 12,
|
|
65
|
+
paddingHorizontal: 16,
|
|
66
|
+
alignItems: "center" as const,
|
|
67
|
+
justifyContent: "center" as const,
|
|
68
|
+
minHeight: 44,
|
|
69
|
+
flexDirection: "row" as const,
|
|
70
|
+
gap: 8,
|
|
71
|
+
},
|
|
72
|
+
secondaryButtonText: {
|
|
73
|
+
color: theme.colorText,
|
|
74
|
+
fontSize: theme.fontSize,
|
|
75
|
+
fontWeight: "500" as const,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/theme.tsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React, { createContext, useContext } from "react"
|
|
2
|
+
|
|
3
|
+
export interface AuthThemeTokens {
|
|
4
|
+
colorPrimary: string
|
|
5
|
+
colorDanger: string
|
|
6
|
+
colorText: string
|
|
7
|
+
colorTextMuted: string
|
|
8
|
+
colorBackground: string
|
|
9
|
+
colorBorder: string
|
|
10
|
+
colorSurface: string
|
|
11
|
+
borderRadius: number
|
|
12
|
+
spacing: number
|
|
13
|
+
fontSize: number
|
|
14
|
+
fontSizeTitle: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const defaultAuthTheme: AuthThemeTokens = {
|
|
18
|
+
colorPrimary: "#0f766e",
|
|
19
|
+
colorDanger: "#b91c1c",
|
|
20
|
+
colorText: "#111827",
|
|
21
|
+
colorTextMuted: "#6b7280",
|
|
22
|
+
colorBackground: "#ffffff",
|
|
23
|
+
colorBorder: "#d1d5db",
|
|
24
|
+
colorSurface: "#f9fafb",
|
|
25
|
+
borderRadius: 8,
|
|
26
|
+
spacing: 16,
|
|
27
|
+
fontSize: 14,
|
|
28
|
+
fontSizeTitle: 20,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const AuthThemeContext = createContext<AuthThemeTokens>(defaultAuthTheme)
|
|
32
|
+
|
|
33
|
+
export interface AuthThemeProviderProps {
|
|
34
|
+
tokens?: Partial<AuthThemeTokens> | undefined
|
|
35
|
+
children: React.ReactNode
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function AuthThemeProvider({ tokens, children }: AuthThemeProviderProps): React.ReactElement {
|
|
39
|
+
const value: AuthThemeTokens = { ...defaultAuthTheme, ...tokens }
|
|
40
|
+
return React.createElement(AuthThemeContext.Provider, { value }, children)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useAuthTheme(): AuthThemeTokens {
|
|
44
|
+
return useContext(AuthThemeContext)
|
|
45
|
+
}
|