feat(auth/ios): native Apple Sign-In via expo-apple-authentication

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 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-18 00:13:45 +02:00
parent 534f978b4e
commit 23cc147231
4 changed files with 75 additions and 34 deletions

View File

@ -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,
},

View File

@ -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,6 +269,7 @@ export default function SignInScreen() {
<Text className="text-neutral-900 text-base" style={{ fontFamily: 'Nunito_600SemiBold' }}>{t('auth.googleSignin')}</Text>
</TouchableOpacity>
{Platform.OS === 'ios' ? (
<TouchableOpacity
onPress={() => onOAuth('apple')}
disabled={isLoading}
@ -282,6 +284,7 @@ export default function SignInScreen() {
)}
<Text className="text-white text-base" style={{ fontFamily: 'Nunito_600SemiBold' }}>{t('auth.appleSignin')}</Text>
</TouchableOpacity>
) : null}
{/* Divider */}
<View className="flex-row items-center mb-6">

View File

@ -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,6 +133,7 @@ export default function SignUpScreen() {
<Text className="text-neutral-900 text-base" style={{ fontFamily: 'Nunito_600SemiBold' }}>{t('auth.googleSignup')}</Text>
</TouchableOpacity>
{Platform.OS === 'ios' ? (
<TouchableOpacity
onPress={() => onOAuth('apple')}
disabled={isLoading}
@ -146,6 +148,7 @@ export default function SignUpScreen() {
)}
<Text className="text-white text-base" style={{ fontFamily: 'Nunito_600SemiBold' }}>{t('auth.appleSignup')}</Text>
</TouchableOpacity>
) : null}
{/* Divider */}
<View className="flex-row items-center mb-6">

View File

@ -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<AuthState>((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: {