@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.
Files changed (56) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test.log +14 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/README.md +66 -0
  5. package/dist/AuthGate.d.ts +15 -0
  6. package/dist/AuthGate.d.ts.map +1 -0
  7. package/dist/AuthGate.js +19 -0
  8. package/dist/AuthGate.js.map +1 -0
  9. package/dist/LoginForm.d.ts +19 -0
  10. package/dist/LoginForm.d.ts.map +1 -0
  11. package/dist/LoginForm.js +45 -0
  12. package/dist/LoginForm.js.map +1 -0
  13. package/dist/MagicLinkForm.d.ts +20 -0
  14. package/dist/MagicLinkForm.d.ts.map +1 -0
  15. package/dist/MagicLinkForm.js +50 -0
  16. package/dist/MagicLinkForm.js.map +1 -0
  17. package/dist/OAuthButton.d.ts +27 -0
  18. package/dist/OAuthButton.d.ts.map +1 -0
  19. package/dist/OAuthButton.js +54 -0
  20. package/dist/OAuthButton.js.map +1 -0
  21. package/dist/ResetPassword.d.ts +28 -0
  22. package/dist/ResetPassword.d.ts.map +1 -0
  23. package/dist/ResetPassword.js +73 -0
  24. package/dist/ResetPassword.js.map +1 -0
  25. package/dist/SignUpForm.d.ts +20 -0
  26. package/dist/SignUpForm.d.ts.map +1 -0
  27. package/dist/SignUpForm.js +55 -0
  28. package/dist/SignUpForm.js.map +1 -0
  29. package/dist/index.d.ts +15 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +8 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/styles.d.ts +76 -0
  34. package/dist/styles.d.ts.map +1 -0
  35. package/dist/styles.js +77 -0
  36. package/dist/styles.js.map +1 -0
  37. package/dist/theme.d.ts +22 -0
  38. package/dist/theme.d.ts.map +1 -0
  39. package/dist/theme.js +23 -0
  40. package/dist/theme.js.map +1 -0
  41. package/package.json +53 -0
  42. package/src/AuthGate.tsx +43 -0
  43. package/src/LoginForm.tsx +142 -0
  44. package/src/MagicLinkForm.tsx +136 -0
  45. package/src/OAuthButton.tsx +99 -0
  46. package/src/ResetPassword.tsx +191 -0
  47. package/src/SignUpForm.tsx +160 -0
  48. package/src/index.ts +24 -0
  49. package/src/react-native.d.ts +103 -0
  50. package/src/styles.ts +78 -0
  51. package/src/theme.tsx +45 -0
  52. package/tests/auth-ui.test.tsx +186 -0
  53. package/tests/mocks/react-native.ts +23 -0
  54. package/tsconfig.json +17 -0
  55. package/tsconfig.tsbuildinfo +1 -0
  56. 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
+ }