From 62f92b094df316206012cecdb53b471419fc5087 Mon Sep 17 00:00:00 2001 From: Cats Juice Date: Wed, 23 Oct 2024 14:56:48 +0800 Subject: [PATCH] feat(mobile): ios oauth & magic-link login --- .../xcshareddata/xcschemes/App.xcscheme | 78 +++++++++++++++++++ packages/frontend/apps/ios/App/App/Info.plist | 17 +++- packages/frontend/apps/ios/App/Podfile | 3 +- packages/frontend/apps/ios/App/Podfile.lock | 14 +++- .../frontend/apps/ios/capacitor.config.ts | 11 +++ packages/frontend/apps/ios/package.json | 2 + packages/frontend/apps/ios/src/app.tsx | 35 +++++++++ packages/frontend/core/package.json | 2 + .../core/src/components/affine/auth/oauth.tsx | 7 ++ .../src/components/affine/auth/send-email.tsx | 6 +- packages/frontend/core/src/mobile/router.tsx | 4 + .../core/src/modules/cloud/services/auth.ts | 7 +- packages/frontend/core/src/utils/popup.ts | 6 ++ tools/cli/src/bin/dev.ts | 3 + yarn.lock | 22 ++++++ 15 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 packages/frontend/apps/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme b/packages/frontend/apps/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme new file mode 100644 index 0000000000000..91a0d15ea213a --- /dev/null +++ b/packages/frontend/apps/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/frontend/apps/ios/App/App/Info.plist b/packages/frontend/apps/ios/App/App/Info.plist index e59b9163f59fe..10b7d08c8b45e 100644 --- a/packages/frontend/apps/ios/App/App/Info.plist +++ b/packages/frontend/apps/ios/App/App/Info.plist @@ -2,8 +2,6 @@ - ITSAppUsesNonExemptEncryption - CFBundleDevelopmentRegion en CFBundleDisplayName @@ -20,8 +18,23 @@ APPL CFBundleShortVersionString $(MARKETING_VERSION) + CFBundleURLTypes + + + CFBundleTypeRole + None + CFBundleURLName + affine + CFBundleURLSchemes + + affine + + + CFBundleVersion 10 + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS UILaunchScreen diff --git a/packages/frontend/apps/ios/App/Podfile b/packages/frontend/apps/ios/App/Podfile index 42b9c05276ec8..06b9627ac4db3 100644 --- a/packages/frontend/apps/ios/App/Podfile +++ b/packages/frontend/apps/ios/App/Podfile @@ -11,7 +11,8 @@ install! 'cocoapods', :disable_input_output_paths => true def capacitor_pods pod 'Capacitor', :path => '../../../../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../../../../node_modules/@capacitor/ios' - + pod 'CapacitorApp', :path => '../../../../../node_modules/@capacitor/app' + pod 'CapacitorBrowser', :path => '../../../../../node_modules/@capacitor/browser' end target 'App' do diff --git a/packages/frontend/apps/ios/App/Podfile.lock b/packages/frontend/apps/ios/App/Podfile.lock index 8d5b740a1cad2..f6a8f3ed5a3a2 100644 --- a/packages/frontend/apps/ios/App/Podfile.lock +++ b/packages/frontend/apps/ios/App/Podfile.lock @@ -1,22 +1,34 @@ PODS: - Capacitor (6.1.2): - CapacitorCordova + - CapacitorApp (6.0.1): + - Capacitor + - CapacitorBrowser (6.0.3): + - Capacitor - CapacitorCordova (6.1.2) DEPENDENCIES: - "Capacitor (from `../../../../../node_modules/@capacitor/ios`)" + - "CapacitorApp (from `../../../../../node_modules/@capacitor/app`)" + - "CapacitorBrowser (from `../../../../../node_modules/@capacitor/browser`)" - "CapacitorCordova (from `../../../../../node_modules/@capacitor/ios`)" EXTERNAL SOURCES: Capacitor: :path: "../../../../../node_modules/@capacitor/ios" + CapacitorApp: + :path: "../../../../../node_modules/@capacitor/app" + CapacitorBrowser: + :path: "../../../../../node_modules/@capacitor/browser" CapacitorCordova: :path: "../../../../../node_modules/@capacitor/ios" SPEC CHECKSUMS: Capacitor: 679f9673fdf30597493a6362a5d5bf233d46abc2 + CapacitorApp: 0bc633b4eae40a1f32cd2834788fad3bc42da6a1 + CapacitorBrowser: aab1ed943b01c0365c4810538a8b3477e2d9f72e CapacitorCordova: f48c89f96c319101cd2f0ce8a2b7449b5fb8b3dd -PODFILE CHECKSUM: 54b94ef731578bd3a2af3619f2a5a0589e32dea5 +PODFILE CHECKSUM: 0f32d90fb8184cf478f85b78b1c00db1059ac3aa COCOAPODS: 1.15.2 diff --git a/packages/frontend/apps/ios/capacitor.config.ts b/packages/frontend/apps/ios/capacitor.config.ts index 56eee290e60ab..236dd793ce3f5 100644 --- a/packages/frontend/apps/ios/capacitor.config.ts +++ b/packages/frontend/apps/ios/capacitor.config.ts @@ -7,6 +7,17 @@ const config: CapacitorConfig = { ios: { path: '.', }, + server: { + // url: 'http://localhost:8080', + }, + plugins: { + CapacitorCookies: { + enabled: true, + }, + CapacitorHttp: { + enabled: true, + }, + }, }; export default config; diff --git a/packages/frontend/apps/ios/package.json b/packages/frontend/apps/ios/package.json index 9f9fd1cbfacb6..de11ba96ec48e 100644 --- a/packages/frontend/apps/ios/package.json +++ b/packages/frontend/apps/ios/package.json @@ -15,6 +15,8 @@ "@affine/i18n": "workspace:*", "@blocksuite/affine": "0.17.19", "@blocksuite/icons": "^2.1.67", + "@capacitor/app": "^6.0.1", + "@capacitor/browser": "^6.0.3", "@capacitor/core": "^6.1.2", "@capacitor/ios": "^6.1.2", "@sentry/react": "^8.0.0", diff --git a/packages/frontend/apps/ios/src/app.tsx b/packages/frontend/apps/ios/src/app.tsx index 4f88ff26cc9ba..a829da0519f63 100644 --- a/packages/frontend/apps/ios/src/app.tsx +++ b/packages/frontend/apps/ios/src/app.tsx @@ -4,6 +4,7 @@ import { Telemetry } from '@affine/core/components/telemetry'; import { configureMobileModules } from '@affine/core/mobile/modules'; import { router } from '@affine/core/mobile/router'; import { configureCommonModules } from '@affine/core/modules'; +import { AuthService } from '@affine/core/modules/cloud'; import { I18nProvider } from '@affine/core/modules/i18n'; import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage'; import { configureIndexedDBUserspaceStorageProvider } from '@affine/core/modules/userspace'; @@ -12,6 +13,8 @@ import { configureBrowserWorkspaceFlavours, configureIndexedDBWorkspaceEngineStorageProvider, } from '@affine/core/modules/workspace-engine'; +import { App as CapacitorApp } from '@capacitor/app'; +import { Browser } from '@capacitor/browser'; import { Framework, FrameworkRoot, @@ -41,6 +44,38 @@ window.addEventListener('focus', () => { }); frameworkProvider.get(LifecycleService).applicationStart(); +CapacitorApp.addListener('appUrlOpen', ({ url }) => { + // try to close browser if it's open + Browser.close().catch(e => console.error('Failed to close browser', e)); + + const urlObj = new URL(url); + + if (urlObj.hostname === 'authentication') { + const method = urlObj.searchParams.get('method'); + const payload = JSON.parse(urlObj.searchParams.get('payload') ?? 'false'); + + if ( + !method || + (method !== 'magic-link' && method !== 'oauth') || + !payload + ) { + console.error('Invalid authentication url', url); + return; + } + + const authService = frameworkProvider.get(AuthService); + if (method === 'oauth') { + authService + .signInOauth(payload.code, payload.state, payload.provider) + .catch(console.error); + } else if (method === 'magic-link') { + authService + .signInMagicLink(payload.email, payload.token) + .catch(console.error); + } + } +}); + export function App() { return ( diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json index d7f4fe2b3e95d..ac28113a0dd1b 100644 --- a/packages/frontend/core/package.json +++ b/packages/frontend/core/package.json @@ -18,6 +18,8 @@ "@affine/track": "workspace:*", "@blocksuite/affine": "0.17.19", "@blocksuite/icons": "2.1.69", + "@capacitor/app": "^6.0.1", + "@capacitor/browser": "^6.0.3", "@dnd-kit/core": "^6.1.0", "@dnd-kit/modifiers": "^7.0.0", "@dnd-kit/sortable": "^8.0.0", diff --git a/packages/frontend/core/src/components/affine/auth/oauth.tsx b/packages/frontend/core/src/components/affine/auth/oauth.tsx index 8f765678ae75d..b6e757fe9fb7c 100644 --- a/packages/frontend/core/src/components/affine/auth/oauth.tsx +++ b/packages/frontend/core/src/components/affine/auth/oauth.tsx @@ -58,6 +58,13 @@ function OAuthProvider({ provider }: { provider: OAuthProviderType }) { oauthUrl += `&client=${appInfo?.schema}`; } + if (BUILD_CONFIG.isIOS) { + // app scheme: "affine" + oauthUrl += `&client=affine`; + } + // TODO: Android app scheme not implemented + // if (BUILD_CONFIG.isAndroid) {} + popupWindow(oauthUrl); }, [provider]); diff --git a/packages/frontend/core/src/components/affine/auth/send-email.tsx b/packages/frontend/core/src/components/affine/auth/send-email.tsx index cea9e1bbdbdec..12606a70f4fc3 100644 --- a/packages/frontend/core/src/components/affine/auth/send-email.tsx +++ b/packages/frontend/core/src/components/affine/auth/send-email.tsx @@ -122,7 +122,11 @@ const useSendEmail = (emailType: AuthPanelProps<'sendEmail'>['emailType']) => { return trigger({ email, callbackUrl: `/auth/${callbackUrl}?isClient=${ - BUILD_CONFIG.isElectron ? 'true' : 'false' + BUILD_CONFIG.isElectron || + BUILD_CONFIG.isIOS || + BUILD_CONFIG.isAndroid + ? 'true' + : 'false' }`, }); }, diff --git a/packages/frontend/core/src/mobile/router.tsx b/packages/frontend/core/src/mobile/router.tsx index ff11999b5351f..2e2ff001851a0 100644 --- a/packages/frontend/core/src/mobile/router.tsx +++ b/packages/frontend/core/src/mobile/router.tsx @@ -85,6 +85,10 @@ export const topLevelRoutes = [ path: '/redirect-proxy', lazy: () => import('@affine/core/desktop/pages/redirect'), }, + { + path: '/open-app/:action', + lazy: () => import('@affine/core/desktop/pages/open-app'), + }, { path: '*', lazy: () => import('./pages/404'), diff --git a/packages/frontend/core/src/modules/cloud/services/auth.ts b/packages/frontend/core/src/modules/cloud/services/auth.ts index b8118c2be35b6..c07cb6620949a 100644 --- a/packages/frontend/core/src/modules/cloud/services/auth.ts +++ b/packages/frontend/core/src/modules/cloud/services/auth.ts @@ -116,13 +116,18 @@ export class AuthService extends Service { ) { track.$.$.auth.signIn({ method: 'magic-link' }); try { + const scheme = BUILD_CONFIG.isElectron + ? appInfo?.schema + : BUILD_CONFIG.isIOS || BUILD_CONFIG.isAndroid + ? 'affine' + : 'web'; await this.fetchService.fetch('/api/auth/sign-in', { method: 'POST', body: JSON.stringify({ email, // we call it [callbackUrl] instead of [redirect_uri] // to make it clear the url is used to finish the sign-in process instead of redirect after signed-in - callbackUrl: `/magic-link?client=${BUILD_CONFIG.isElectron ? appInfo?.schema : 'web'}`, + callbackUrl: `/magic-link?client=${scheme}`, }), headers: { 'content-type': 'application/json', diff --git a/packages/frontend/core/src/utils/popup.ts b/packages/frontend/core/src/utils/popup.ts index 04e6088fd21ac..0bcc0cfdee301 100644 --- a/packages/frontend/core/src/utils/popup.ts +++ b/packages/frontend/core/src/utils/popup.ts @@ -1,5 +1,6 @@ import { DebugLogger } from '@affine/debug'; import { apis } from '@affine/electron-api'; +import { Browser } from '@capacitor/browser'; const logger = new DebugLogger('popup'); @@ -35,6 +36,11 @@ export function popupWindow(target: string) { apis?.ui.openExternal(url).catch(e => { logger.error('Failed to open external URL', e); }); + } else if (BUILD_CONFIG.isIOS || BUILD_CONFIG.isAndroid) { + Browser.open({ + url, + presentationStyle: 'popover', + }).catch(console.error); } else { window.open(url, '_blank', `noreferrer noopener`); } diff --git a/tools/cli/src/bin/dev.ts b/tools/cli/src/bin/dev.ts index 7e435e7d2a47a..8962d797e263b 100644 --- a/tools/cli/src/bin/dev.ts +++ b/tools/cli/src/bin/dev.ts @@ -52,6 +52,9 @@ const buildFlags = process.argv.includes('--static') { value: 'mobile', }, + { + value: 'ios', + }, ], initialValue: 'web', }), diff --git a/yarn.lock b/yarn.lock index 3045393045cde..653096524bc58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -389,6 +389,8 @@ __metadata: "@affine/track": "workspace:*" "@blocksuite/affine": "npm:0.17.19" "@blocksuite/icons": "npm:2.1.69" + "@capacitor/app": "npm:^6.0.1" + "@capacitor/browser": "npm:^6.0.3" "@dnd-kit/core": "npm:^6.1.0" "@dnd-kit/modifiers": "npm:^7.0.0" "@dnd-kit/sortable": "npm:^8.0.0" @@ -596,6 +598,8 @@ __metadata: "@affine/i18n": "workspace:*" "@blocksuite/affine": "npm:0.17.19" "@blocksuite/icons": "npm:^2.1.67" + "@capacitor/app": "npm:^6.0.1" + "@capacitor/browser": "npm:^6.0.3" "@capacitor/cli": "npm:^6.1.2" "@capacitor/core": "npm:^6.1.2" "@capacitor/ios": "npm:^6.1.2" @@ -2908,6 +2912,24 @@ __metadata: languageName: node linkType: hard +"@capacitor/app@npm:^6.0.1": + version: 6.0.1 + resolution: "@capacitor/app@npm:6.0.1" + peerDependencies: + "@capacitor/core": ^6.0.0 + checksum: 10/3e8bc85c3a43728c003c80511dde913872b1263dbc27931b1168f52442b8d95f35e5a6a85423d6ac61253a5e4b2198bbb7b899cae9f5e343edfed24010399ade + languageName: node + linkType: hard + +"@capacitor/browser@npm:^6.0.3": + version: 6.0.3 + resolution: "@capacitor/browser@npm:6.0.3" + peerDependencies: + "@capacitor/core": ^6.0.0 + checksum: 10/e19e66c12b5c3a05e0c4a93715c0bec760781ddeb215a93da0a9ee3e9e4ede2f2893d399ffb8d7b5ded6ac70651ab482689ff57e5e0d067cd4398cc3247c4f81 + languageName: node + linkType: hard + "@capacitor/cli@npm:^6.1.2": version: 6.1.2 resolution: "@capacitor/cli@npm:6.1.2"