feat: support native sign-in-with-apple on macOS (#856)

This commit is contained in:
Huang Xin
2025-04-10 20:49:11 +08:00
committed by GitHub
parent d4af2d3ef5
commit fe25c3e995
8 changed files with 308 additions and 21 deletions
+16 -11
View File
@@ -43,6 +43,7 @@ interface ProviderLoginProp {
const WEB_AUTH_CALLBACK = `${READEST_WEB_BASE_URL}/auth/callback`;
const DEEPLINK_CALLBACK = 'readest://auth-callback';
const USE_APPLE_SIGN_IN = process.env['NEXT_PUBLIC_USE_APPLE_SIGN_IN'] === 'true';
const ProviderLogin: React.FC<ProviderLoginProp> = ({ provider, handleSignIn, Icon, label }) => {
return (
@@ -74,7 +75,7 @@ export default function AuthPage() {
const headerRef = useRef<HTMLDivElement>(null);
const getTauriRedirectTo = (isOAuth: boolean) => {
if (process.env.NODE_ENV === 'production' || appService?.isMobile) {
if (process.env.NODE_ENV === 'production' || appService?.isMobile || USE_APPLE_SIGN_IN) {
if (appService?.isMobile) {
return isOAuth ? DEEPLINK_CALLBACK : WEB_AUTH_CALLBACK;
}
@@ -97,15 +98,19 @@ export default function AuthPage() {
const request = {
scope: ['fullName', 'email'] as Scope[],
};
const appleAuthResponse = await getAppleIdAuth(request);
if (appleAuthResponse.identityToken) {
const { error } = await supabase.auth.signInWithIdToken({
provider: 'apple',
token: appleAuthResponse.identityToken,
});
if (error) {
console.error('Authentication error:', error);
if (appService?.isIOSApp || USE_APPLE_SIGN_IN) {
const appleAuthResponse = await getAppleIdAuth(request);
if (appleAuthResponse.identityToken) {
const { error } = await supabase.auth.signInWithIdToken({
provider: 'apple',
token: appleAuthResponse.identityToken,
});
if (error) {
console.error('Authentication error:', error);
}
}
} else {
console.log('Sign in with Apple on this platform is not supported yet');
}
};
@@ -161,7 +166,7 @@ export default function AuthPage() {
const startTauriOAuth = async () => {
try {
if (process.env.NODE_ENV === 'production' || appService?.isMobile) {
if (process.env.NODE_ENV === 'production' || appService?.isMobile || USE_APPLE_SIGN_IN) {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const currentWindow = getCurrentWindow();
currentWindow.listen('single-instance', ({ event, payload }) => {
@@ -345,7 +350,7 @@ export default function AuthPage() {
/>
<ProviderLogin
provider='apple'
handleSignIn={appService?.isIOSApp ? tauriSignInApple : tauriSignIn}
handleSignIn={appService?.isIOSApp || USE_APPLE_SIGN_IN ? tauriSignInApple : tauriSignIn}
Icon={FaApple}
label={_('Sign in with Apple')}
/>
@@ -1,4 +1,6 @@
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { type as osType } from '@tauri-apps/plugin-os';
export type Scope = 'fullName' | 'email';
export interface AppleIDAuthorizationRequest {
@@ -20,15 +22,67 @@ export interface AppleIDAuthorizationResponse {
state: string | null;
}
type NativeSuccess = {
authorization_code: string;
identity_token: string | null;
user_identifier: string | null;
given_name: string | null;
family_name: string | null;
email: string | null;
state: string | null;
};
export async function getAppleIdAuth(
request: AppleIDAuthorizationRequest,
): Promise<AppleIDAuthorizationResponse> {
const result = await invoke<AppleIDAuthorizationResponse>(
'plugin:sign-in-with-apple|get_apple_id_credential',
{
payload: request,
},
);
const OS_TYPE = osType();
if (OS_TYPE === 'ios') {
const result = await invoke<AppleIDAuthorizationResponse>(
'plugin:sign-in-with-apple|get_apple_id_credential',
{
payload: request,
},
);
return result;
return result;
} else if (OS_TYPE === 'macos') {
return new Promise<AppleIDAuthorizationResponse>(async (resolve, reject) => {
const unlistenComplete = await listen<NativeSuccess>(
'apple-sign-in-complete',
({ payload }) => {
cleanup();
resolve({
userIdentifier: payload.user_identifier,
authorizationCode: payload.authorization_code,
identityToken: payload.identity_token,
givenName: payload.given_name,
familyName: payload.family_name,
email: payload.email,
state: payload.state,
});
},
);
const unlistenError = await listen<string>('apple-sign-in-error', ({ payload }) => {
cleanup();
reject(
typeof payload === 'string' ? new Error(payload) : new Error('Apple signin failed'),
);
});
function cleanup() {
unlistenComplete();
unlistenError();
}
try {
await invoke('start_apple_sign_in', { payload: request });
} catch (err) {
cleanup();
reject(err);
}
});
} else {
throw new Error('Unsupported platform');
}
}
@@ -19,7 +19,6 @@ interface DemoBooks {
export const useDemoBooks = () => {
const { envConfig } = useEnv();
const userLang = getUserLang() as keyof typeof libraries;
const [books, setBooks] = useState<Book[]>([]);
const isLoading = useRef(false);
@@ -27,6 +26,7 @@ export const useDemoBooks = () => {
if (isLoading.current) return;
isLoading.current = true;
const userLang = getUserLang() as keyof typeof libraries;
const fetchDemoBooks = async () => {
try {
const appService = await envConfig.getAppService();