@fastshot/auth 1.0.6 → 1.1.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/package.json +4 -5
- package/src/index.ts +64 -80
- package/src/provider/AuthCallbackPage.tsx +140 -0
- package/src/provider/AuthProvider.tsx +821 -0
- package/src/provider/index.ts +5 -0
- package/src/provider/layouts.tsx +182 -0
- package/src/types.ts +55 -0
- package/src/utils/deepLink.ts +28 -2
- package/src/utils/session.ts +50 -32
- package/src/utils/ticketExchange.ts +78 -4
- package/src/auth.ts +0 -206
- package/src/hooks/index.ts +0 -8
- package/src/hooks/useAppleSignIn.ts +0 -94
- package/src/hooks/useAuthCallback.ts +0 -135
- package/src/hooks/useGoogleSignIn.ts +0 -97
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastshot/auth",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "OAuth authentication SDK for Expo React Native applications with Supabase",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"repository": {
|
|
@@ -23,15 +23,14 @@
|
|
|
23
23
|
"babel-plugin-transform-inline-environment-variables": "^0.4.4"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"react": "^19.0.0",
|
|
27
|
-
"react-native": ">=0.70.0",
|
|
28
26
|
"@supabase/supabase-js": "^2.0.0",
|
|
29
27
|
"expo-linking": "^8.0.0",
|
|
28
|
+
"expo-router": "^6.0.21",
|
|
30
29
|
"expo-web-browser": "^15.0.0"
|
|
31
30
|
},
|
|
32
31
|
"devDependencies": {
|
|
33
|
-
"@types/react": "~19.
|
|
34
|
-
"react": "19.
|
|
32
|
+
"@types/react": "~19.2.0",
|
|
33
|
+
"react": "19.2.3",
|
|
35
34
|
"react-native": "0.81.5",
|
|
36
35
|
"typescript": "~5.9.2"
|
|
37
36
|
},
|
package/src/index.ts
CHANGED
|
@@ -1,103 +1,87 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fastshot/auth - OAuth authentication
|
|
2
|
+
* @fastshot/auth - Opinionated OAuth authentication for React Native Expo
|
|
3
3
|
*
|
|
4
4
|
* Provides Google and Apple OAuth authentication via Fastshot's auth-broker,
|
|
5
|
-
* with automatic Supabase session
|
|
5
|
+
* with automatic Supabase session management. Designed for Expo Router.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* ```tsx
|
|
9
|
-
* // Using hooks (recommended)
|
|
10
|
-
* import { useGoogleSignIn, useAppleSignIn, useAuthCallback } from '@fastshot/auth';
|
|
11
|
-
* import { supabase } from './lib/supabase';
|
|
7
|
+
* ## Quick Start (2 Steps!)
|
|
12
8
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
9
|
+
* ### Step 1: Wrap your app with AuthProvider + routes config
|
|
10
|
+
* ```tsx
|
|
11
|
+
* // app/_layout.tsx - This is ALL you need for auth routing!
|
|
12
|
+
* import { Stack } from 'expo-router';
|
|
13
|
+
* import { AuthProvider } from '@fastshot/auth';
|
|
14
|
+
* import { supabase } from '@/lib/supabase';
|
|
19
15
|
*
|
|
20
|
-
*
|
|
16
|
+
* export default function RootLayout() {
|
|
17
|
+
* return (
|
|
18
|
+
* <AuthProvider
|
|
19
|
+
* supabaseClient={supabase}
|
|
20
|
+
* routes={{
|
|
21
|
+
* login: '/(auth)/login',
|
|
22
|
+
* afterLogin: '/(tabs)',
|
|
23
|
+
* }}
|
|
24
|
+
* >
|
|
25
|
+
* <Stack screenOptions={{ headerShown: false }}>
|
|
26
|
+
* <Stack.Screen name="(auth)" />
|
|
27
|
+
* <Stack.Screen name="(tabs)" />
|
|
28
|
+
* </Stack>
|
|
29
|
+
* </AuthProvider>
|
|
30
|
+
* );
|
|
21
31
|
* }
|
|
32
|
+
* ```
|
|
22
33
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* const { signIn: appleSignIn, isLoading: appleLoading } = useAppleSignIn({
|
|
29
|
-
* supabaseClient: supabase,
|
|
30
|
-
* });
|
|
34
|
+
* ### Step 2: Use auth in your screens
|
|
35
|
+
* ```tsx
|
|
36
|
+
* // app/(auth)/login.tsx
|
|
37
|
+
* import { useAuth } from '@fastshot/auth';
|
|
31
38
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* <Button title="Sign in with Apple" onPress={appleSignIn} />
|
|
36
|
-
* </View>
|
|
37
|
-
* );
|
|
39
|
+
* export default function LoginScreen() {
|
|
40
|
+
* const { signInWithGoogle, isLoading } = useAuth();
|
|
41
|
+
* return <Button onPress={signInWithGoogle} disabled={isLoading}>Sign In</Button>;
|
|
38
42
|
* }
|
|
39
43
|
* ```
|
|
40
44
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* import { signInWithGoogle, handleAuthCallback } from '@fastshot/auth';
|
|
45
|
-
* import { supabase } from './lib/supabase';
|
|
45
|
+
* That's it! Auth redirects are automatic:
|
|
46
|
+
* - Unauthenticated users visiting `(tabs)` → redirected to login
|
|
47
|
+
* - Authenticated users visiting `(auth)` → redirected to tabs
|
|
46
48
|
*
|
|
47
|
-
*
|
|
48
|
-
* await signInWithGoogle({ supabaseClient: supabase });
|
|
49
|
+
* ## Features
|
|
49
50
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
51
|
+
* - **Minimal setup**: Just add `routes` config, redirects are automatic
|
|
52
|
+
* - **Convention-based**: Common route names auto-detected (tabs, app, auth, etc.)
|
|
53
|
+
* - **Expo Router native**: Designed for file-based routing with route groups
|
|
54
|
+
* - **Reactive state**: UI automatically updates when auth state changes
|
|
55
|
+
* - **Deep link handling**: Handles OAuth callbacks automatically
|
|
56
|
+
* - **TypeScript**: Full type safety out of the box
|
|
53
57
|
*/
|
|
54
58
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
signInWithApple,
|
|
59
|
-
handleAuthCallback,
|
|
60
|
-
signOut,
|
|
61
|
-
} from './auth';
|
|
62
|
-
export type { SignInOptions } from './auth';
|
|
59
|
+
// =============================================================================
|
|
60
|
+
// PRIMARY API
|
|
61
|
+
// =============================================================================
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
UseAuthCallbackConfig,
|
|
76
|
-
UseAuthCallbackResult,
|
|
77
|
-
} from './hooks';
|
|
63
|
+
/**
|
|
64
|
+
* Primary exports - these are what most users need
|
|
65
|
+
*
|
|
66
|
+
* - AuthProvider: Wrap your app to enable auth
|
|
67
|
+
* - useAuth: Hook to access auth state and methods
|
|
68
|
+
* - ProtectedLayout: Wrapper for routes requiring authentication
|
|
69
|
+
* - GuestLayout: Wrapper for routes that should only be accessible when logged out
|
|
70
|
+
* - AuthCallbackPage: Web callback page component (only needed for web)
|
|
71
|
+
*/
|
|
72
|
+
export { AuthProvider, useAuth, AuthCallbackPage, ProtectedLayout, GuestLayout } from './provider';
|
|
73
|
+
export type { AuthProviderConfig, AuthContextValue, SignUpOptions, ProtectedLayoutProps, GuestLayoutProps } from './provider';
|
|
78
74
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
isAuthCallbackUrl,
|
|
83
|
-
parseCallbackUrl,
|
|
84
|
-
getDefaultCallbackUrl,
|
|
85
|
-
createAuthError,
|
|
86
|
-
isAuthError,
|
|
87
|
-
} from './utils';
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// TYPES
|
|
77
|
+
// =============================================================================
|
|
88
78
|
|
|
89
|
-
// Types
|
|
90
79
|
export type {
|
|
91
|
-
OAuthProvider,
|
|
92
|
-
AuthMode,
|
|
93
80
|
AuthError,
|
|
94
81
|
AuthErrorType,
|
|
95
|
-
|
|
96
|
-
|
|
82
|
+
OAuthProvider,
|
|
83
|
+
AuthMode,
|
|
84
|
+
SignUpResult,
|
|
85
|
+
PasswordResetResult,
|
|
86
|
+
AuthRoutes,
|
|
97
87
|
} from './types';
|
|
98
|
-
|
|
99
|
-
// Constants (for debugging/customization)
|
|
100
|
-
export { AUTH_CONFIG, AUTH_ENDPOINTS } from './constants';
|
|
101
|
-
|
|
102
|
-
// Environment utilities
|
|
103
|
-
export { getProjectId, getAuthBrokerUrl, ENV } from './env';
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthCallbackPage - Web OAuth callback handler
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```tsx
|
|
6
|
+
* // app/auth/callback.tsx
|
|
7
|
+
* import { AuthCallbackPage } from '@fastshot/auth';
|
|
8
|
+
* import { supabase } from '@/lib/supabase';
|
|
9
|
+
* import { useRouter } from 'expo-router';
|
|
10
|
+
*
|
|
11
|
+
* export default function Callback() {
|
|
12
|
+
* const router = useRouter();
|
|
13
|
+
* return (
|
|
14
|
+
* <AuthCallbackPage
|
|
15
|
+
* supabaseClient={supabase}
|
|
16
|
+
* onSuccess={() => router.replace('/')}
|
|
17
|
+
* />
|
|
18
|
+
* );
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import React, { useEffect, useState } from 'react';
|
|
24
|
+
import { View, Text, ActivityIndicator, StyleSheet, Platform, Pressable } from 'react-native';
|
|
25
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
26
|
+
import { parseCallbackUrl, exchangeTicket, restoreSession, isAuthError } from '../utils';
|
|
27
|
+
|
|
28
|
+
interface AuthCallbackPageProps {
|
|
29
|
+
supabaseClient: SupabaseClient;
|
|
30
|
+
onSuccess?: () => void;
|
|
31
|
+
onError?: (error: Error) => void;
|
|
32
|
+
loadingText?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type Status = 'loading' | 'success' | 'error';
|
|
36
|
+
|
|
37
|
+
// Module-level: track which ticket is being processed to prevent duplicates
|
|
38
|
+
let processingTicket: string | null = null;
|
|
39
|
+
|
|
40
|
+
export function AuthCallbackPage({
|
|
41
|
+
supabaseClient,
|
|
42
|
+
onSuccess,
|
|
43
|
+
onError,
|
|
44
|
+
loadingText = 'Completing sign in...',
|
|
45
|
+
}: AuthCallbackPageProps) {
|
|
46
|
+
const [status, setStatus] = useState<Status>('loading');
|
|
47
|
+
const [error, setError] = useState<string | null>(null);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (Platform.OS !== 'web' || typeof window === 'undefined') return;
|
|
51
|
+
|
|
52
|
+
const { ticket, error: urlError, errorDescription } = parseCallbackUrl(window.location.href);
|
|
53
|
+
|
|
54
|
+
if (urlError) {
|
|
55
|
+
setError(errorDescription || urlError);
|
|
56
|
+
setStatus('error');
|
|
57
|
+
onError?.(new Error(errorDescription || urlError));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!ticket) {
|
|
62
|
+
setError('No ticket found in URL');
|
|
63
|
+
setStatus('error');
|
|
64
|
+
onError?.(new Error('No ticket found'));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Prevent duplicate processing
|
|
69
|
+
if (processingTicket === ticket) return;
|
|
70
|
+
processingTicket = ticket;
|
|
71
|
+
|
|
72
|
+
// Process the OAuth callback
|
|
73
|
+
processCallback(ticket);
|
|
74
|
+
|
|
75
|
+
async function processCallback(ticket: string) {
|
|
76
|
+
try {
|
|
77
|
+
const tokens = await exchangeTicket(ticket);
|
|
78
|
+
await restoreSession(supabaseClient, tokens);
|
|
79
|
+
|
|
80
|
+
processingTicket = null;
|
|
81
|
+
setStatus('success');
|
|
82
|
+
|
|
83
|
+
// Navigate on next tick
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
onSuccess ? onSuccess() : window.location.replace('/');
|
|
86
|
+
}, 50);
|
|
87
|
+
|
|
88
|
+
} catch (err) {
|
|
89
|
+
processingTicket = null;
|
|
90
|
+
const msg = isAuthError(err) ? err.message : (err instanceof Error ? err.message : 'Failed');
|
|
91
|
+
console.error('[@fastshot/auth] Callback failed:', err);
|
|
92
|
+
setError(msg);
|
|
93
|
+
setStatus('error');
|
|
94
|
+
onError?.(new Error(msg));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}, [supabaseClient, onSuccess, onError]);
|
|
98
|
+
|
|
99
|
+
const navigate = () => {
|
|
100
|
+
onSuccess ? onSuccess() : window.location.replace('/');
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<View style={styles.container}>
|
|
105
|
+
{status === 'loading' && (
|
|
106
|
+
<>
|
|
107
|
+
<ActivityIndicator size="large" color="#4A7C59" />
|
|
108
|
+
<Text style={styles.text}>{loadingText}</Text>
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
111
|
+
{status === 'success' && (
|
|
112
|
+
<>
|
|
113
|
+
<Text style={styles.success}>Sign in successful!</Text>
|
|
114
|
+
<Pressable onPress={navigate} style={styles.button}>
|
|
115
|
+
<Text style={styles.buttonText}>Continue →</Text>
|
|
116
|
+
</Pressable>
|
|
117
|
+
</>
|
|
118
|
+
)}
|
|
119
|
+
{status === 'error' && (
|
|
120
|
+
<>
|
|
121
|
+
<Text style={styles.error}>{error}</Text>
|
|
122
|
+
<Pressable onPress={() => window.history.back()} style={styles.link}>
|
|
123
|
+
<Text style={styles.linkText}>← Go back</Text>
|
|
124
|
+
</Pressable>
|
|
125
|
+
</>
|
|
126
|
+
)}
|
|
127
|
+
</View>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const styles = StyleSheet.create({
|
|
132
|
+
container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', padding: 20 },
|
|
133
|
+
text: { marginTop: 16, fontSize: 16, color: '#666' },
|
|
134
|
+
success: { fontSize: 20, fontWeight: '600', color: '#16a34a', marginBottom: 16 },
|
|
135
|
+
error: { fontSize: 16, color: '#dc2626', textAlign: 'center', marginBottom: 16 },
|
|
136
|
+
button: { paddingHorizontal: 24, paddingVertical: 12, backgroundColor: '#4A7C59', borderRadius: 8 },
|
|
137
|
+
buttonText: { color: '#fff', fontSize: 16, fontWeight: '600' },
|
|
138
|
+
link: { padding: 12 },
|
|
139
|
+
linkText: { color: '#4A7C59', fontSize: 16 },
|
|
140
|
+
});
|