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>
117 lines
4.4 KiB
TypeScript
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 },
|
|
});
|