@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastshot/auth",
3
- "version": "1.0.6",
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.1.10",
34
- "react": "19.1.0",
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 SDK for Expo React Native
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 restoration.
5
+ * with automatic Supabase session management. Designed for Expo Router.
6
6
  *
7
- * @example
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
- * function App() {
14
- * // Handle OAuth callbacks automatically
15
- * useAuthCallback({
16
- * supabaseClient: supabase,
17
- * onSuccess: ({ user }) => console.log('Logged in:', user.email),
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
- * return <LoginScreen />;
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
- * function LoginScreen() {
24
- * const { signIn: googleSignIn, isLoading: googleLoading } = useGoogleSignIn({
25
- * supabaseClient: supabase,
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
- * return (
33
- * <View>
34
- * <Button title="Sign in with Google" onPress={googleSignIn} />
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
- * @example
42
- * ```typescript
43
- * // Using standalone functions
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
- * // Trigger sign-in
48
- * await signInWithGoogle({ supabaseClient: supabase });
49
+ * ## Features
49
50
  *
50
- * // Handle callback (in your deep link handler)
51
- * const result = await handleAuthCallback(url, supabase);
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
- // Core auth functions
56
- export {
57
- signInWithGoogle,
58
- signInWithApple,
59
- handleAuthCallback,
60
- signOut,
61
- } from './auth';
62
- export type { SignInOptions } from './auth';
59
+ // =============================================================================
60
+ // PRIMARY API
61
+ // =============================================================================
63
62
 
64
- // React hooks
65
- export {
66
- useGoogleSignIn,
67
- useAppleSignIn,
68
- useAuthCallback,
69
- } from './hooks';
70
- export type {
71
- UseGoogleSignInConfig,
72
- UseGoogleSignInResult,
73
- UseAppleSignInConfig,
74
- UseAppleSignInResult,
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
- // Utilities (for advanced usage)
80
- export {
81
- exchangeTicket,
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
- AuthCallbackResult,
96
- ExchangeTicketResponse,
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
+ });