Files
petfeed/mobile/src/components/AppleSignInButton.tsx
T
adminandClaude Opus 5.5 7169840000 fix(mobile): Apple-Login hängt nicht mehr still bei fehlenden Pflichtfeldern
Fehlen nach dem Apple-Login noch Pflichtangaben (z. B. Vor-/Nachname,
die Apple nur bei der allerersten Autorisierung schickt), zeigt der
Button jetzt eine Fehlermeldung und meldet die Felder an Sentry, statt
kommentarlos nichts zu tun.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 20:01:19 +02:00

117 lines
4.4 KiB
TypeScript

import { useEffect, useState } from "react";
import { Platform, View, StyleSheet } from "react-native";
import { useRouter } from "expo-router";
import { useTranslation } from "react-i18next";
import * as AppleAuthentication from "expo-apple-authentication";
import { useSignInWithApple } from "@clerk/expo/apple";
import * as Sentry from "@sentry/react-native";
import { useTheme } from "../context/ThemeContext";
import { radii } from "../theme/tokens";
type Props = {
onError: (message: string) => void;
/** Same contract as GoogleSignInButton's `legalAccepted` — see there. */
legalAccepted?: boolean;
/** Same contract as GoogleSignInButton's `onNeedsConsent` — see there. */
onNeedsConsent?: () => void;
disabled?: boolean;
};
/**
* "Sign in with Apple" — required by App Store Review Guideline 4.8 because
* the app offers Google login. iOS only: renders nothing on Android or on an
* iOS device where Apple Authentication isn't available.
*
* Uses Clerk's native `useSignInWithApple()` (expo-apple-authentication
* sheet + `oauth_token_apple` token exchange) instead of the browser-based
* `oauth_apple` SSO flow — Apple expects the native sheet, and it avoids the
* redirect-URL pitfalls the Google SSO flow had.
*
* Uses Apple's own AppleAuthenticationButton (HIG-mandated look), not a
* custom Pressable like GoogleSignInButton. Legal-consent handling mirrors
* GoogleSignInButton: Clerk's sign-up comes back `missing_requirements`
* with `legal_accepted` for brand-new accounts.
*/
export function AppleSignInButton(props: Props) {
const [isAvailable, setIsAvailable] = useState(false);
useEffect(() => {
if (Platform.OS !== "ios") return;
AppleAuthentication.isAvailableAsync().then(setIsAvailable).catch(() => setIsAvailable(false));
}, []);
if (!isAvailable) return null;
return <AppleSignInButtonInner {...props} />;
}
function AppleSignInButtonInner({ onError, legalAccepted, onNeedsConsent, disabled }: Props) {
const { t } = useTranslation();
const { scheme } = useTheme();
const router = useRouter();
const { startAppleAuthenticationFlow } = useSignInWithApple();
const [isPending, setIsPending] = useState(false);
const onPress = async () => {
setIsPending(true);
try {
const result = await startAppleAuthenticationFlow();
const { setActive, signUp } = result;
let createdSessionId = result.createdSessionId;
let missingFields = signUp?.status === "missing_requirements" ? signUp.missingFields : [];
if (!createdSessionId && signUp && missingFields.includes("legal_accepted")) {
if (!legalAccepted) {
onNeedsConsent?.();
return;
}
const updatedSignUp = await signUp.update({ legalAccepted: true });
createdSessionId = updatedSignUp.createdSessionId;
missingFields = updatedSignUp.status === "missing_requirements" ? updatedSignUp.missingFields : [];
}
if (createdSessionId && setActive) {
await setActive({ session: createdSessionId });
router.replace("/(app)");
return;
}
// Any other required field Apple can't supply (e.g. first/last name —
// Apple only sends the name on the very first authorization) would
// otherwise leave the button silently doing nothing.
if (missingFields.length > 0) {
Sentry.captureMessage(`Apple sign-up missing requirements: ${missingFields.join(", ")}`);
onError(t("auth.appleSignInError"));
}
// No createdSessionId and nothing missing: user cancelled the Apple sheet.
} catch (err) {
Sentry.captureException(err);
onError(err instanceof Error ? err.message : t("auth.appleSignInError"));
} finally {
setIsPending(false);
}
};
const isInactive = isPending || !!disabled;
return (
<View style={isInactive && styles.disabled} pointerEvents={isInactive ? "none" : "auto"}>
<AppleAuthentication.AppleAuthenticationButton
buttonType={AppleAuthentication.AppleAuthenticationButtonType.CONTINUE}
buttonStyle={
scheme === "dark"
? AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
: AppleAuthentication.AppleAuthenticationButtonStyle.BLACK
}
cornerRadius={radii.lg}
style={styles.button}
onPress={onPress}
/>
</View>
);
}
const styles = StyleSheet.create({
button: { height: 48, width: "100%" },
disabled: { opacity: 0.5 },
});