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"