← back to CelebritySignatures
celeb mobile: add Sign in with Apple button to AccountScreen — appleSignIn() runs ASWebAuthenticationSession (expo-web-browser) then replays /auth/handoff so cs_sess lands in the native cookie jar; button hidden until /api/auth-config reports apple:true (v1.1, staged — not submitted)
5c6df61c0be2c96e63b0d732b3d8eede704fd1b1 · 2026-08-07 10:08:25 -0700 · Steve
Files touched
M apps/mobile/api.tsM apps/mobile/app.jsonM apps/mobile/package-lock.jsonM apps/mobile/package.jsonM apps/mobile/screens/AccountScreen.tsx
Diff
commit 5c6df61c0be2c96e63b0d732b3d8eede704fd1b1
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Aug 7 10:08:25 2026 -0700
celeb mobile: add Sign in with Apple button to AccountScreen — appleSignIn() runs ASWebAuthenticationSession (expo-web-browser) then replays /auth/handoff so cs_sess lands in the native cookie jar; button hidden until /api/auth-config reports apple:true (v1.1, staged — not submitted)
---
apps/mobile/api.ts | 29 +++++++++++++++++++
apps/mobile/app.json | 3 +-
apps/mobile/package-lock.json | 11 +++++++
apps/mobile/package.json | 1 +
apps/mobile/screens/AccountScreen.tsx | 54 ++++++++++++++++++++++++++++++++++-
5 files changed, 96 insertions(+), 2 deletions(-)
diff --git a/apps/mobile/api.ts b/apps/mobile/api.ts
index b38d85f..5b795e0 100644
--- a/apps/mobile/api.ts
+++ b/apps/mobile/api.ts
@@ -3,10 +3,37 @@
// own cookie jar, so the cs_sess auth cookie set by /api/auth/* is retained
// across fetches automatically (no CORS constraints apply to a native client).
import Constants from 'expo-constants';
+import * as WebBrowser from 'expo-web-browser';
export const WEB_BASE: string =
(Constants.expoConfig?.extra as any)?.webBase ?? 'https://celebsignatures.com';
+/**
+ * Sign in with Apple (native). Apple/Google reject OAuth inside an embedded
+ * WebView, so we run the web sign-in flow in ASWebAuthenticationSession, then
+ * replay the one-time handoff token from THIS native fetch — so the cs_sess
+ * cookie lands in RN's shared cookie jar (the same jar every api call + the
+ * checkout WebView use). Returns {ok:false, error:'canceled'} on user cancel.
+ */
+export async function appleSignIn(): Promise<{ ok: boolean; error?: string }> {
+ try {
+ const result = await WebBrowser.openAuthSessionAsync(
+ `${WEB_BASE}/auth/apple/login?app=1`,
+ 'celebsignatures://auth',
+ );
+ if (result.type === 'cancel' || result.type === 'dismiss') return { ok: false, error: 'canceled' };
+ if (result.type !== 'success' || !result.url) return { ok: false, error: 'Sign in was not completed.' };
+ const token = new URL(result.url).searchParams.get('token');
+ if (!token) return { ok: false, error: 'No sign-in token returned.' };
+ // Replay the handoff (302 + Set-Cookie); RN's fetch stores the cookie from
+ // the redirect chain into the shared native jar.
+ await fetch(`${WEB_BASE}/auth/handoff?token=${encodeURIComponent(token)}`);
+ return { ok: true };
+ } catch (e) {
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
+ }
+}
+
export type Signature = {
category: string;
rank?: number;
@@ -86,6 +113,8 @@ export const api = {
leaderboardSubmit: (game: string, diff: string, score: number, name: string) =>
postJSON<{ ok: boolean; rank?: number; top?: LeaderRow[] }>('/api/leaderboard', { game, diff, score, name }),
+ // Which sign-in methods are live server-side (Apple button hides until configured).
+ authConfig: () => getJSON<{ ok: boolean; apple: boolean }>('/api/auth-config'),
me: () => getJSON<{ ok: boolean; user: User }>('/api/auth/me'),
login: (email: string, password: string) =>
postJSON<{ ok: boolean; user?: User; error?: string }>('/api/auth/login', { email, password }),
diff --git a/apps/mobile/app.json b/apps/mobile/app.json
index 7fb671f..d8dd7fd 100644
--- a/apps/mobile/app.json
+++ b/apps/mobile/app.json
@@ -32,7 +32,8 @@
"imageWidth": 220,
"resizeMode": "contain"
}
- ]
+ ],
+ "expo-web-browser"
],
"extra": {
"webBase": "https://celebsignatures.com",
diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json
index f07f411..615777a 100644
--- a/apps/mobile/package-lock.json
+++ b/apps/mobile/package-lock.json
@@ -16,6 +16,7 @@
"expo-linear-gradient": "~56.0.4",
"expo-splash-screen": "~56.0.14",
"expo-status-bar": "~56.0.4",
+ "expo-web-browser": "~56.0.6",
"react": "19.2.3",
"react-native": "0.85.3",
"react-native-webview": "13.16.1"
@@ -2810,6 +2811,16 @@
"react-native": "*"
}
},
+ "node_modules/expo-web-browser": {
+ "version": "56.0.6",
+ "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-56.0.6.tgz",
+ "integrity": "sha512-+5Zg0JHETAB1+vNu0Bb4+WiUB50MViwcaJKt16sz5GJTrnutcJvwFeXr6oJ7Pw73sp+DjxkIDGSHDq8SAmFRSg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo/node_modules/@expo/cli": {
"version": "56.1.22",
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-56.1.22.tgz",
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index ee4b2cc..eb71b6b 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -17,6 +17,7 @@
"expo-linear-gradient": "~56.0.4",
"expo-splash-screen": "~56.0.14",
"expo-status-bar": "~56.0.4",
+ "expo-web-browser": "~56.0.6",
"react": "19.2.3",
"react-native": "0.85.3",
"react-native-webview": "13.16.1"
diff --git a/apps/mobile/screens/AccountScreen.tsx b/apps/mobile/screens/AccountScreen.tsx
index ba89f62..135d640 100644
--- a/apps/mobile/screens/AccountScreen.tsx
+++ b/apps/mobile/screens/AccountScreen.tsx
@@ -21,7 +21,7 @@ import {
} from 'react-native';
import * as Haptics from 'expo-haptics';
-import { api, User } from '../api';
+import { api, User, appleSignIn } from '../api';
import { GLASS, GlassButton, GlassPanel } from '../ui/glass';
// ---------------------------------------------------------------------------
@@ -121,10 +121,40 @@ function AuthView({ onSuccess }: { onSuccess: (user: User) => void }) {
const [name, setName] = useState('');
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
+ const [appleBusy, setAppleBusy] = useState(false);
+ const [appleAvailable, setAppleAvailable] = useState(false);
const passwordRef = useRef<TextInput>(null);
const nameRef = useRef<TextInput>(null);
+ // Show the Apple button only once its creds are configured server-side.
+ useEffect(() => {
+ api.authConfig().then((c) => setAppleAvailable(!!c.apple)).catch(() => setAppleAvailable(false));
+ }, []);
+
+ const handleApple = useCallback(async () => {
+ setAppleBusy(true);
+ setError(null);
+ try {
+ const r = await appleSignIn();
+ if (r.ok) {
+ const me = await api.me();
+ if (me.ok && me.user) {
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
+ onSuccess(me.user);
+ return;
+ }
+ setError('Signed in, but could not load your account. Please try again.');
+ } else if (r.error && r.error !== 'canceled') {
+ setError(r.error);
+ }
+ } catch {
+ setError('Network error. Please check your connection.');
+ } finally {
+ setAppleBusy(false);
+ }
+ }, [onSuccess]);
+
// Reset error when switching mode
const handleModeChange = useCallback((m: AuthMode) => {
setMode(m);
@@ -225,6 +255,18 @@ function AuthView({ onSuccess }: { onSuccess: (user: User) => void }) {
disabled={busy}
/>
</View>
+
+ {appleAvailable && (
+ <View style={styles.appleWrap}>
+ <Text style={styles.orText}>or</Text>
+ <GlassButton
+ label={' Sign in with Apple'}
+ onPress={handleApple}
+ busy={appleBusy}
+ disabled={appleBusy || busy}
+ />
+ </View>
+ )}
</View>
);
}
@@ -505,6 +547,16 @@ const styles = StyleSheet.create({
submitBtn: {
marginTop: 8,
},
+ appleWrap: {
+ marginTop: 14,
+ gap: 8,
+ },
+ orText: {
+ textAlign: 'center',
+ color: GLASS.textDim,
+ fontSize: 13,
+ letterSpacing: 0.3,
+ },
// Signed-in view
signedInRoot: {
← ee2b355 celeb: add Sign in with Apple backend (web OAuth + WebView h
·
back to CelebritySignatures
·
celeb: allow /.well-known/ through the sensitive-path guard 7f435c2 →