feat(mobile/ios): iOS simulator build profile and native Sign in with Apple
- eas.json: new `preview-simulator` profile (extends preview, ios.simulator) for EAS cloud simulator builds without an Apple Developer account - app.json: iOS deployment target 17.0 via expo-build-properties (@clerk/expo's ClerkKit SPM package requires iOS 17 — pod install failed on the default 16.4), iPhone-only (supportsTablet: false), usesAppleSignIn, ITSAppUsesNonExemptEncryption: false - AppleSignInButton: native Apple sheet via Clerk's useSignInWithApple (App Store Guideline 4.8, since Google login is offered); same legal-consent handling as GoogleSignInButton; iOS-only - Not yet testable end-to-end: needs Apple Developer account, the Sign in with Apple capability on the App ID and Apple enabled in Clerk Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
070c389786
commit
1e7d76aa79
9 files changed
+175
-9
No files matched your search
+16
-3
@@ -8,8 +8,12 @@
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "org.pawfeed.app"
|
||||
"supportsTablet": false,
|
||||
"bundleIdentifier": "org.pawfeed.app",
|
||||
"usesAppleSignIn": true,
|
||||
"infoPlist": {
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"package": "org.pawfeed.app",
|
||||
@@ -64,7 +68,16 @@
|
||||
}
|
||||
],
|
||||
"expo-web-browser",
|
||||
"@sentry/react-native"
|
||||
"expo-apple-authentication",
|
||||
"@sentry/react-native",
|
||||
[
|
||||
"expo-build-properties",
|
||||
{
|
||||
"ios": {
|
||||
"deploymentTarget": "17.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"extra": {
|
||||
"router": {},
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TextField } from "../../src/components/TextField";
|
||||
import { PasswordField } from "../../src/components/PasswordField";
|
||||
import { LanguageSwitcher } from "../../src/components/LanguageSwitcher";
|
||||
import { GoogleSignInButton } from "../../src/components/GoogleSignInButton";
|
||||
import { AppleSignInButton } from "../../src/components/AppleSignInButton";
|
||||
import { useTheme } from "../../src/context/ThemeContext";
|
||||
import { getTypography, type ColorTokens, type Typography } from "../../src/theme/tokens";
|
||||
|
||||
@@ -107,6 +108,10 @@ export default function SignInScreen() {
|
||||
onError={setError}
|
||||
onNeedsConsent={() => setError(t("auth.googleNeedsSignUpError"))}
|
||||
/>
|
||||
<AppleSignInButton
|
||||
onError={setError}
|
||||
onNeedsConsent={() => setError(t("auth.appleNeedsSignUpError"))}
|
||||
/>
|
||||
<Text style={styles.divider}>{t("auth.orDivider")}</Text>
|
||||
<TextField
|
||||
autoCapitalize="none"
|
||||
|
||||
@@ -9,6 +9,7 @@ import { TextField } from "../../src/components/TextField";
|
||||
import { PasswordField } from "../../src/components/PasswordField";
|
||||
import { LanguageSwitcher } from "../../src/components/LanguageSwitcher";
|
||||
import { GoogleSignInButton } from "../../src/components/GoogleSignInButton";
|
||||
import { AppleSignInButton } from "../../src/components/AppleSignInButton";
|
||||
import { useTheme } from "../../src/context/ThemeContext";
|
||||
import { env } from "../../src/lib/env";
|
||||
import { getTypography, type ColorTokens, type Typography } from "../../src/theme/tokens";
|
||||
@@ -127,6 +128,7 @@ export default function SignUpScreen() {
|
||||
<LanguageSwitcher />
|
||||
<Text style={styles.title}>{t("auth.signUpTitle")}</Text>
|
||||
<GoogleSignInButton onError={setError} legalAccepted={legalAccepted} disabled={!legalAccepted} />
|
||||
<AppleSignInButton onError={setError} legalAccepted={legalAccepted} disabled={!legalAccepted} />
|
||||
<Text style={styles.divider}>{t("auth.orDivider")}</Text>
|
||||
<TextField
|
||||
placeholder={t("auth.firstNamePlaceholder")}
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
},
|
||||
"channel": "preview"
|
||||
},
|
||||
"preview-simulator": {
|
||||
"extends": "preview",
|
||||
"ios": {
|
||||
"simulator": true
|
||||
}
|
||||
},
|
||||
"production": {
|
||||
"android": {
|
||||
"buildType": "app-bundle"
|
||||
|
||||
Generated
+32
-4
@@ -17,8 +17,11 @@
|
||||
"@trpc/client": "11.17.0",
|
||||
"@trpc/tanstack-react-query": "11.17.0",
|
||||
"expo": "~57.0.22",
|
||||
"expo-apple-authentication": "~57.0.2",
|
||||
"expo-auth-session": "~57.0.12",
|
||||
"expo-build-properties": "~57.0.22",
|
||||
"expo-constants": "~57.0.16",
|
||||
"expo-crypto": "~57.0.3",
|
||||
"expo-dev-client": "~57.0.19",
|
||||
"expo-file-system": "~57.0.6",
|
||||
"expo-font": "~57.0.3",
|
||||
@@ -6311,6 +6314,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/expo-apple-authentication": {
|
||||
"version": "57.0.2",
|
||||
"resolved": "https://registry.npmjs.org/expo-apple-authentication/-/expo-apple-authentication-57.0.2.tgz",
|
||||
"integrity": "sha512-00KnlyRceRKski+ljZz8q8JhJZ4D3ZScU+Y0RqS6ElJ8GaCWfd7Qh5tMr47yIU/dKDrOnkzkFUM+6vyHS5HI7Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-application": {
|
||||
"version": "57.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expo-application/-/expo-application-57.0.3.tgz",
|
||||
@@ -6353,11 +6367,16 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-auth-session/node_modules/expo-crypto": {
|
||||
"version": "57.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-57.0.3.tgz",
|
||||
"integrity": "sha512-SAWqEfF37nc8ot7bXsVmu9AFYwQdC2kGvH/eNAHiR3bmlcf1Vv8wu9AnGKT+a90r3T8YfwMpZP4QAYlo4vCn9w==",
|
||||
"node_modules/expo-build-properties": {
|
||||
"version": "57.0.22",
|
||||
"resolved": "https://registry.npmjs.org/expo-build-properties/-/expo-build-properties-57.0.22.tgz",
|
||||
"integrity": "sha512-A5oRf2I02XDRu2NnqKo7Au5kUu244unvyoRMUsB+xvg/BOlsR+G/oyDFGkjbda1cJbeKm9eqposIXbA2ri5UrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/schema-utils": "^57.0.2",
|
||||
"resolve-from": "^5.0.0",
|
||||
"semver": "^7.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
@@ -6375,6 +6394,15 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-crypto": {
|
||||
"version": "57.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-57.0.3.tgz",
|
||||
"integrity": "sha512-SAWqEfF37nc8ot7bXsVmu9AFYwQdC2kGvH/eNAHiR3bmlcf1Vv8wu9AnGKT+a90r3T8YfwMpZP4QAYlo4vCn9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-dev-client": {
|
||||
"version": "57.0.19",
|
||||
"resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-57.0.19.tgz",
|
||||
|
||||
@@ -12,8 +12,11 @@
|
||||
"@trpc/client": "11.17.0",
|
||||
"@trpc/tanstack-react-query": "11.17.0",
|
||||
"expo": "~57.0.22",
|
||||
"expo-apple-authentication": "~57.0.2",
|
||||
"expo-auth-session": "~57.0.12",
|
||||
"expo-build-properties": "~57.0.22",
|
||||
"expo-constants": "~57.0.16",
|
||||
"expo-crypto": "~57.0.3",
|
||||
"expo-dev-client": "~57.0.19",
|
||||
"expo-file-system": "~57.0.6",
|
||||
"expo-font": "~57.0.3",
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
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;
|
||||
|
||||
if (!createdSessionId && signUp?.status === "missing_requirements" && signUp.missingFields.includes("legal_accepted")) {
|
||||
if (!legalAccepted) {
|
||||
onNeedsConsent?.();
|
||||
return;
|
||||
}
|
||||
const updatedSignUp = await signUp.update({ legalAccepted: true });
|
||||
createdSessionId = updatedSignUp.createdSessionId;
|
||||
}
|
||||
|
||||
if (createdSessionId && setActive) {
|
||||
await setActive({ session: createdSessionId });
|
||||
router.replace("/(app)");
|
||||
}
|
||||
// No createdSessionId otherwise (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 },
|
||||
});
|
||||
@@ -240,7 +240,9 @@
|
||||
"googleButton": "Mit Google anmelden",
|
||||
"orDivider": "oder",
|
||||
"googleSignInError": "Anmeldung mit Google fehlgeschlagen. Bitte versuche es erneut.",
|
||||
"googleNeedsSignUpError": "Für dieses Google-Konto existiert noch kein PawFeed-Account. Bitte über \"Registrieren\" fortfahren, um den Nutzungsbedingungen zuzustimmen."
|
||||
"googleNeedsSignUpError": "Für dieses Google-Konto existiert noch kein PawFeed-Account. Bitte über \"Registrieren\" fortfahren, um den Nutzungsbedingungen zuzustimmen.",
|
||||
"appleSignInError": "Anmeldung mit Apple fehlgeschlagen. Bitte versuche es erneut.",
|
||||
"appleNeedsSignUpError": "Für diese Apple-ID existiert noch kein PawFeed-Account. Bitte über \"Registrieren\" fortfahren, um den Nutzungsbedingungen zuzustimmen."
|
||||
},
|
||||
"onboarding": {
|
||||
"addPetTitle": "Tier hinzufügen",
|
||||
|
||||
@@ -240,7 +240,9 @@
|
||||
"googleButton": "Continue with Google",
|
||||
"orDivider": "or",
|
||||
"googleSignInError": "Google sign-in failed. Please try again.",
|
||||
"googleNeedsSignUpError": "There's no PawFeed account for this Google account yet. Please continue via \"Sign up\" to accept the Terms of Service."
|
||||
"googleNeedsSignUpError": "There's no PawFeed account for this Google account yet. Please continue via \"Sign up\" to accept the Terms of Service.",
|
||||
"appleSignInError": "Apple sign-in failed. Please try again.",
|
||||
"appleNeedsSignUpError": "There's no PawFeed account for this Apple ID yet. Please continue via \"Sign up\" to accept the Terms of Service."
|
||||
},
|
||||
"onboarding": {
|
||||
"addPetTitle": "Add pet",
|
||||
|
||||
Reference in new issue
Block a user