From 23cc1472311e565b361e2d8e05e5fc19946a94b3 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Mon, 18 May 2026 00:13:45 +0200 Subject: [PATCH] feat(auth/ios): native Apple Sign-In via expo-apple-authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vorher: stores/auth.ts hatte TODO + fiel auf Supabase-Web-OAuth-Flow zurück, was fehlschlug mit 400 'Unsupported provider: missing OAuth client ID' weil der Supabase-Apple-OAuth-Provider nicht konfiguriert ist. Jetzt: native Flow ohne Supabase-Provider-Config — - expo-apple-authentication.signInAsync() → identityToken - supabase.auth.signInWithIdToken({provider:'apple', token}) verifiziert direkt gegen Apple's Public-Keys (kein Client-Secret-JWT-Setup nötig) - User-Cancel (ERR_REQUEST_CANCELED) → leeres Resultat statt Error - Platform-Guard: Apple-Path nur auf iOS app.config.ts: ios.usesAppleSignIn=true → Expo prebuild generiert das com.apple.developer.applesignin-Entitlement in die .entitlements. Beim ersten EAS-Build wird die Capability auto-registriert im Apple-Developer- Portal für org.rebreak.app. signin.tsx + signup.tsx: Apple-Button conditional auf Platform.OS==='ios' gerendert. Android-User sehen nur Google-Button (auf Android gibt es kein natives Apple Sign-In). App-Store-Submission-Pflicht (Apple Guideline 4.8 — wer OAuth-Login mit 3rd-Party-Provider anbietet, muss auch Apple Sign-In bieten). Co-Authored-By: Claude Opus 4.7 --- apps/rebreak-native/app.config.ts | 4 +++ apps/rebreak-native/app/(auth)/signin.tsx | 31 ++++++++-------- apps/rebreak-native/app/(auth)/signup.tsx | 31 ++++++++-------- apps/rebreak-native/stores/auth.ts | 43 +++++++++++++++++++---- 4 files changed, 75 insertions(+), 34 deletions(-) diff --git a/apps/rebreak-native/app.config.ts b/apps/rebreak-native/app.config.ts index bae81f6..94adcbc 100644 --- a/apps/rebreak-native/app.config.ts +++ b/apps/rebreak-native/app.config.ts @@ -21,6 +21,10 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ supportsTablet: true, bundleIdentifier: "org.rebreak.app", buildNumber: "10", + // Apple Sign-In Entitlement — Pflicht für expo-apple-authentication nativen + // signInAsync()-Flow. Ohne flag generiert Expo's prebuild den + // com.apple.developer.applesignin-Entitlement nicht in die .entitlements. + usesAppleSignIn: true, config: { usesNonExemptEncryption: false, }, diff --git a/apps/rebreak-native/app/(auth)/signin.tsx b/apps/rebreak-native/app/(auth)/signin.tsx index 254f729..1e92674 100644 --- a/apps/rebreak-native/app/(auth)/signin.tsx +++ b/apps/rebreak-native/app/(auth)/signin.tsx @@ -5,6 +5,7 @@ import { TextInput, TouchableOpacity, ActivityIndicator, + Platform, } from 'react-native'; import { useRouter } from 'expo-router'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -268,20 +269,22 @@ export default function SignInScreen() { {t('auth.googleSignin')} - onOAuth('apple')} - disabled={isLoading} - activeOpacity={0.8} - className="flex-row items-center justify-center gap-3 bg-neutral-900 rounded-xl mb-6 disabled:opacity-40" - style={{ paddingVertical: 14 }} - > - {oauthLoading === 'apple' ? ( - - ) : ( - - )} - {t('auth.appleSignin')} - + {Platform.OS === 'ios' ? ( + onOAuth('apple')} + disabled={isLoading} + activeOpacity={0.8} + className="flex-row items-center justify-center gap-3 bg-neutral-900 rounded-xl mb-6 disabled:opacity-40" + style={{ paddingVertical: 14 }} + > + {oauthLoading === 'apple' ? ( + + ) : ( + + )} + {t('auth.appleSignin')} + + ) : null} {/* Divider */} diff --git a/apps/rebreak-native/app/(auth)/signup.tsx b/apps/rebreak-native/app/(auth)/signup.tsx index 45d5428..c6feef2 100644 --- a/apps/rebreak-native/app/(auth)/signup.tsx +++ b/apps/rebreak-native/app/(auth)/signup.tsx @@ -6,6 +6,7 @@ import { TouchableOpacity, Image, ActivityIndicator, + Platform, } from 'react-native'; import { useRouter } from 'expo-router'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -132,20 +133,22 @@ export default function SignUpScreen() { {t('auth.googleSignup')} - onOAuth('apple')} - disabled={isLoading} - activeOpacity={0.8} - className="flex-row items-center justify-center gap-3 bg-neutral-900 rounded-xl mb-6 disabled:opacity-40" - style={{ paddingVertical: 14 }} - > - {oauthLoading === 'apple' ? ( - - ) : ( - - )} - {t('auth.appleSignup')} - + {Platform.OS === 'ios' ? ( + onOAuth('apple')} + disabled={isLoading} + activeOpacity={0.8} + className="flex-row items-center justify-center gap-3 bg-neutral-900 rounded-xl mb-6 disabled:opacity-40" + style={{ paddingVertical: 14 }} + > + {oauthLoading === 'apple' ? ( + + ) : ( + + )} + {t('auth.appleSignup')} + + ) : null} {/* Divider */} diff --git a/apps/rebreak-native/stores/auth.ts b/apps/rebreak-native/stores/auth.ts index 319047e..22b7de0 100644 --- a/apps/rebreak-native/stores/auth.ts +++ b/apps/rebreak-native/stores/auth.ts @@ -1,7 +1,9 @@ import { create } from 'zustand'; +import { Platform } from 'react-native'; import type { Session, User } from '@supabase/supabase-js'; import * as WebBrowser from 'expo-web-browser'; import * as Linking from 'expo-linking'; +import * as AppleAuthentication from 'expo-apple-authentication'; import { supabase } from '../lib/supabase'; import { apiFetch } from '../lib/api'; @@ -129,15 +131,44 @@ export const useAuthStore = create((set) => ({ }, signInWithOAuth: async (provider) => { - const redirectUri = Linking.createURL('auth/callback'); - + // Apple Sign-In native flow (iOS only) — KEIN Supabase-Apple-OAuth-Provider- + // Config nötig (Bundle-ID = Apple-Client-ID). expo-apple-authentication + // liefert identityToken → supabase.auth.signInWithIdToken verifiziert direkt + // gegen Apple's public keys. App-Store-Submit-Pflicht weil Google-Sign-In + // auch da ist (Apple Guideline 4.8). if (provider === 'apple') { - // TODO: configure Apple Sign-In - // Requires expo-apple-authentication to be installed + Apple Developer entitlement. - // Apple Client ID = Bundle ID (org.rebreak.app) for native flow. - // For now we fall through to the Supabase OAuth web flow as a temporary path. + if (Platform.OS !== 'ios') { + return { error: 'Apple Sign-In ist nur auf iOS verfügbar.' }; + } + const available = await AppleAuthentication.isAvailableAsync(); + if (!available) { + return { error: 'Apple Sign-In auf diesem Gerät nicht verfügbar.' }; + } + try { + const credential = await AppleAuthentication.signInAsync({ + requestedScopes: [ + AppleAuthentication.AppleAuthenticationScope.FULL_NAME, + AppleAuthentication.AppleAuthenticationScope.EMAIL, + ], + }); + if (!credential.identityToken) { + return { error: 'Apple identityToken fehlt.' }; + } + const { data, error } = await supabase.auth.signInWithIdToken({ + provider: 'apple', + token: credential.identityToken, + }); + if (error) return { error: error.message }; + set({ session: data.session, user: data.user ?? null }); + return {}; + } catch (e: any) { + // User-Cancel ist kein Error — leere Antwort. + if (e?.code === 'ERR_REQUEST_CANCELED') return {}; + return { error: e?.message ?? 'Apple Sign-In fehlgeschlagen.' }; + } } + const redirectUri = Linking.createURL('auth/callback'); const { data, error } = await supabase.auth.signInWithOAuth({ provider, options: {