From 06dda70319d7bc2a962ac0429cfb383b759a9c3a Mon Sep 17 00:00:00 2001 From: Cats Juice Date: Mon, 28 Oct 2024 14:12:33 +0800 Subject: [PATCH] feat(mobile): ios oauth & magic-link login (#8581) Co-authored-by: EYHN --- .../infra/src/framework/core/framework.ts | 9 +- .../infra/src/framework/core/provider.ts | 2 +- .../infra/src/framework/react/index.tsx | 6 +- .../components/use-user-management.ts | 2 +- .../frontend/apps/electron/renderer/app.tsx | 17 ++ .../ios/App/App.xcodeproj/project.pbxproj | 106 ++++++---- .../xcshareddata/xcschemes/App.xcscheme | 78 +++++++ .../ios/App/App/AFFiNEViewController.swift | 4 + packages/frontend/apps/ios/App/App/Info.plist | 17 +- .../App/plugins/Cookie/CookieManager.swift | 30 +++ .../App/App/plugins/Cookie/CookiePlugin.swift | 21 ++ 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 | 67 ++++++ packages/frontend/apps/ios/src/fetch.ts | 194 ++++++++++++++++++ packages/frontend/apps/ios/src/index.tsx | 9 + .../ios/src/plugins/cookie/definitions.ts | 6 + .../apps/ios/src/plugins/cookie/index.ts | 8 + packages/frontend/apps/mobile/src/app.tsx | 20 ++ packages/frontend/apps/web/src/app.tsx | 20 ++ packages/frontend/core/package.json | 2 + .../core/src/commands/affine-help.tsx | 6 +- .../core/src/components/affine/auth/oauth.tsx | 26 ++- .../src/components/affine/auth/send-email.tsx | 4 +- .../affine/page-history-modal/data.ts | 25 ++- .../general-setting/about/index.tsx | 8 +- .../appearance/theme-editor-setting.tsx | 9 +- .../general-setting/billing/index.tsx | 12 +- .../general-setting/plans/checkout-slot.tsx | 6 +- .../src/components/hooks/use-app-updater.ts | 9 +- .../hooks/use-register-workspace-commands.ts | 5 +- .../src/components/pure/help-island/index.tsx | 7 +- .../pages/workspace/share/share-page.tsx | 6 +- packages/frontend/core/src/mobile/router.tsx | 4 + .../views/app-updater-button/index.tsx | 7 +- .../frontend/core/src/modules/cloud/index.ts | 20 +- .../core/src/modules/cloud/provider/fetch.ts | 16 ++ .../modules/cloud/provider/websocket-auth.ts | 22 ++ .../core/src/modules/cloud/services/auth.ts | 14 +- .../core/src/modules/cloud/services/fetch.ts | 20 +- .../src/modules/cloud/services/websocket.ts | 21 +- .../src/modules/cloud/stores/subscription.ts | 17 +- packages/frontend/core/src/modules/index.ts | 2 + .../frontend/core/src/modules/url/index.ts | 20 ++ .../modules/url/providers/client-schema.ts | 12 ++ .../src/modules/url/providers/popup-window.ts | 13 ++ .../core/src/modules/url/services/url.ts | 31 +++ .../services/workbench-new-tab-handler.ts | 3 +- .../modules/workspace-engine/impls/cloud.ts | 15 +- .../impls/engine/blob-cloud.ts | 23 ++- .../impls/engine/doc-cloud-static.ts | 14 +- .../src/modules/workspace-engine/index.ts | 2 + packages/frontend/core/src/utils/index.ts | 2 - packages/frontend/core/src/utils/popup.ts | 41 ---- packages/frontend/core/src/utils/url.ts | 33 --- tools/cli/src/bin/dev.ts | 3 + yarn.lock | 22 ++ 59 files changed, 929 insertions(+), 219 deletions(-) create mode 100644 packages/frontend/apps/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme create mode 100644 packages/frontend/apps/ios/App/App/plugins/Cookie/CookieManager.swift create mode 100644 packages/frontend/apps/ios/App/App/plugins/Cookie/CookiePlugin.swift create mode 100644 packages/frontend/apps/ios/src/fetch.ts create mode 100644 packages/frontend/apps/ios/src/plugins/cookie/definitions.ts create mode 100644 packages/frontend/apps/ios/src/plugins/cookie/index.ts create mode 100644 packages/frontend/core/src/modules/cloud/provider/fetch.ts create mode 100644 packages/frontend/core/src/modules/cloud/provider/websocket-auth.ts create mode 100644 packages/frontend/core/src/modules/url/index.ts create mode 100644 packages/frontend/core/src/modules/url/providers/client-schema.ts create mode 100644 packages/frontend/core/src/modules/url/providers/popup-window.ts create mode 100644 packages/frontend/core/src/modules/url/services/url.ts delete mode 100644 packages/frontend/core/src/utils/popup.ts delete mode 100644 packages/frontend/core/src/utils/url.ts diff --git a/packages/common/infra/src/framework/core/framework.ts b/packages/common/infra/src/framework/core/framework.ts index 30f1173d46..de0e53e441 100644 --- a/packages/common/infra/src/framework/core/framework.ts +++ b/packages/common/infra/src/framework/core/framework.ts @@ -1,4 +1,3 @@ -import type { Component } from './components/component'; import type { Entity } from './components/entity'; import type { Scope } from './components/scope'; import type { Service } from './components/service'; @@ -408,8 +407,6 @@ class FrameworkEditor { * * @example * ```ts - * override(OriginClass, NewClass, [dependencies, ...]) - * or * override(Identifier, Class, [dependencies, ...]) * or * override(Identifier, Instance) @@ -418,10 +415,10 @@ class FrameworkEditor { * ``` */ override = < - Arg1 extends GeneralIdentifier, - Arg2 extends Type | ComponentFactory | Trait | null, + Arg1 extends Identifier, + Arg2 extends Type | ComponentFactory | Trait, Arg3 extends Deps, - Trait extends Component = IdentifierType, + Trait = IdentifierType, Deps = Arg2 extends Type ? TypesToDeps> : [], diff --git a/packages/common/infra/src/framework/core/provider.ts b/packages/common/infra/src/framework/core/provider.ts index 15b2419aea..d64c75a65d 100644 --- a/packages/common/infra/src/framework/core/provider.ts +++ b/packages/common/infra/src/framework/core/provider.ts @@ -55,7 +55,7 @@ export abstract class FrameworkProvider { getOptional = ( identifier: GeneralIdentifier, options?: ResolveOptions - ): T | null => { + ): T | undefined => { return this.getRaw(parseIdentifier(identifier), { ...options, optional: true, diff --git a/packages/common/infra/src/framework/react/index.tsx b/packages/common/infra/src/framework/react/index.tsx index 367b6e1189..ff21a2704c 100644 --- a/packages/common/infra/src/framework/react/index.tsx +++ b/packages/common/infra/src/framework/react/index.tsx @@ -23,7 +23,7 @@ export function useService( ): T { const stack = useContext(FrameworkStackContext); - let service: T | null = null; + let service: T | undefined = undefined; for (let i = stack.length - 1; i >= 0; i--) { service = stack[i].getOptional(identifier, { @@ -87,10 +87,10 @@ export function useServices< export function useServiceOptional( identifier: Type -): T | null { +): T | undefined { const stack = useContext(FrameworkStackContext); - let service: T | null = null; + let service: T | undefined = undefined; for (let i = stack.length - 1; i >= 0; i--) { service = stack[i].getOptional(identifier, { diff --git a/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts b/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts index b76c2a3a06..983a008685 100644 --- a/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts +++ b/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts @@ -116,7 +116,7 @@ export const useResetUserPassword = () => { setResetPasswordLink(''); resetPassword({ userId: id, - callbackUrl: '/auth/changePassword?isClient=false', + callbackUrl: '/auth/changePassword', }) .then(res => { setResetPasswordLink(res.createChangePasswordUrl); diff --git a/packages/frontend/apps/electron/renderer/app.tsx b/packages/frontend/apps/electron/renderer/app.tsx index f8c0afc251..3971c39256 100644 --- a/packages/frontend/apps/electron/renderer/app.tsx +++ b/packages/frontend/apps/electron/renderer/app.tsx @@ -9,6 +9,10 @@ import { configureAppTabsHeaderModule } from '@affine/core/modules/app-tabs-head import { I18nProvider } from '@affine/core/modules/i18n'; import { configureElectronStateStorageImpls } from '@affine/core/modules/storage'; import { CustomThemeModifier } from '@affine/core/modules/theme-editor'; +import { + ClientSchemaProvider, + PopupWindowProvider, +} from '@affine/core/modules/url'; import { configureSqliteUserspaceStorageProvider } from '@affine/core/modules/userspace'; import { configureDesktopWorkbenchModule } from '@affine/core/modules/workbench'; import { @@ -16,6 +20,7 @@ import { configureSqliteWorkspaceEngineStorageProvider, } from '@affine/core/modules/workspace-engine'; import createEmotionCache from '@affine/core/utils/create-emotion-cache'; +import { apis, appInfo } from '@affine/electron-api'; import { CacheProvider } from '@emotion/react'; import { Framework, @@ -58,6 +63,18 @@ configureSqliteWorkspaceEngineStorageProvider(framework); configureSqliteUserspaceStorageProvider(framework); configureDesktopWorkbenchModule(framework); configureAppTabsHeaderModule(framework); +framework.impl(PopupWindowProvider, { + open: (url: string) => { + apis?.ui.openExternal(url).catch(e => { + console.error('Failed to open external URL', e); + }); + }, +}); +framework.impl(ClientSchemaProvider, { + getClientSchema() { + return appInfo?.schema; + }, +}); const frameworkProvider = framework.provider(); // setup application lifecycle events, and emit application start event diff --git a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj index 3a9b648ffe..f97a0346e3 100644 --- a/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj +++ b/packages/frontend/apps/ios/App/App.xcodeproj/project.pbxproj @@ -7,28 +7,32 @@ objects = { /* Begin PBXBuildFile section */ - 2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; - 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; - 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; - 504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; }; - 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; - 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; + 9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE172CCB9876006677DB /* CookieManager.swift */; }; + 9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE182CCB9876006677DB /* CookiePlugin.swift */; }; + 9D90BE272CCB9876006677DB /* AFFiNEViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */; }; + 9D90BE282CCB9876006677DB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE1C2CCB9876006677DB /* AppDelegate.swift */; }; + 9D90BE292CCB9876006677DB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1D2CCB9876006677DB /* Assets.xcassets */; }; + 9D90BE2A2CCB9876006677DB /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1E2CCB9876006677DB /* capacitor.config.json */; }; + 9D90BE2B2CCB9876006677DB /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1F2CCB9876006677DB /* config.xml */; }; + 9D90BE2D2CCB9876006677DB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE222CCB9876006677DB /* Main.storyboard */; }; + 9D90BE2E2CCB9876006677DB /* public in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE232CCB9876006677DB /* public */; }; C4C413792CBE705D00337889 /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; }; - E9D409712CCA317C00B06598 /* AFFiNEViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D409702CCA317C00B06598 /* AFFiNEViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; - 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; 504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; + 9D90BE172CCB9876006677DB /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = ""; }; + 9D90BE182CCB9876006677DB /* CookiePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiePlugin.swift; sourceTree = ""; }; + 9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AFFiNEViewController.swift; sourceTree = ""; }; + 9D90BE1C2CCB9876006677DB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 9D90BE1D2CCB9876006677DB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 9D90BE1E2CCB9876006677DB /* capacitor.config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; + 9D90BE1F2CCB9876006677DB /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; + 9D90BE202CCB9876006677DB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9D90BE212CCB9876006677DB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 9D90BE232CCB9876006677DB /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = ""; }; - E9D409702CCA317C00B06598 /* AFFiNEViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AFFiNEViewController.swift; sourceTree = ""; }; FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -55,7 +59,7 @@ 504EC2FB1FED79650016851F = { isa = PBXGroup; children = ( - 504EC3061FED79650016851F /* App */, + 9D90BE242CCB9876006677DB /* App */, 504EC3051FED79650016851F /* Products */, 7F8756D8B27F46E3366F6CEA /* Pods */, 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */, @@ -72,21 +76,6 @@ name = Products; sourceTree = ""; }; - 504EC3061FED79650016851F /* App */ = { - isa = PBXGroup; - children = ( - 50379B222058CBB4000EE86E /* capacitor.config.json */, - 504EC3071FED79650016851F /* AppDelegate.swift */, - 504EC30B1FED79650016851F /* Main.storyboard */, - 504EC30E1FED79650016851F /* Assets.xcassets */, - 504EC3131FED79650016851F /* Info.plist */, - 2FAD9762203C412B000D30F8 /* config.xml */, - 50B271D01FEDC1A000F3C39B /* public */, - E9D409702CCA317C00B06598 /* AFFiNEViewController.swift */, - ); - path = App; - sourceTree = ""; - }; 7F8756D8B27F46E3366F6CEA /* Pods */ = { isa = PBXGroup; children = ( @@ -96,6 +85,39 @@ name = Pods; sourceTree = ""; }; + 9D90BE192CCB9876006677DB /* Cookie */ = { + isa = PBXGroup; + children = ( + 9D90BE172CCB9876006677DB /* CookieManager.swift */, + 9D90BE182CCB9876006677DB /* CookiePlugin.swift */, + ); + path = Cookie; + sourceTree = ""; + }; + 9D90BE1A2CCB9876006677DB /* plugins */ = { + isa = PBXGroup; + children = ( + 9D90BE192CCB9876006677DB /* Cookie */, + ); + path = plugins; + sourceTree = ""; + }; + 9D90BE242CCB9876006677DB /* App */ = { + isa = PBXGroup; + children = ( + 9D90BE1A2CCB9876006677DB /* plugins */, + 9D90BE1B2CCB9876006677DB /* AFFiNEViewController.swift */, + 9D90BE1C2CCB9876006677DB /* AppDelegate.swift */, + 9D90BE1D2CCB9876006677DB /* Assets.xcassets */, + 9D90BE1E2CCB9876006677DB /* capacitor.config.json */, + 9D90BE1F2CCB9876006677DB /* config.xml */, + 9D90BE202CCB9876006677DB /* Info.plist */, + 9D90BE222CCB9876006677DB /* Main.storyboard */, + 9D90BE232CCB9876006677DB /* public */, + ); + path = App; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -130,7 +152,7 @@ TargetAttributes = { 504EC3031FED79650016851F = { CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; + LastSwiftMigration = 1600; }; }; }; @@ -157,11 +179,11 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 50B271D11FEDC1A000F3C39B /* public in Resources */, - 504EC30F1FED79650016851F /* Assets.xcassets in Resources */, - 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */, - 504EC30D1FED79650016851F /* Main.storyboard in Resources */, - 2FAD9763203C412B000D30F8 /* config.xml in Resources */, + 9D90BE292CCB9876006677DB /* Assets.xcassets in Resources */, + 9D90BE2A2CCB9876006677DB /* capacitor.config.json in Resources */, + 9D90BE2B2CCB9876006677DB /* config.xml in Resources */, + 9D90BE2D2CCB9876006677DB /* Main.storyboard in Resources */, + 9D90BE2E2CCB9876006677DB /* public in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -208,18 +230,20 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, - E9D409712CCA317C00B06598 /* AFFiNEViewController.swift in Sources */, + 9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */, + 9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */, + 9D90BE272CCB9876006677DB /* AFFiNEViewController.swift in Sources */, + 9D90BE282CCB9876006677DB /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ - 504EC30B1FED79650016851F /* Main.storyboard */ = { + 9D90BE222CCB9876006677DB /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( - 504EC30C1FED79650016851F /* Base */, + 9D90BE212CCB9876006677DB /* Base */, ); name = Main.storyboard; sourceTree = ""; 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 0000000000..91a0d15ea2 --- /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/AFFiNEViewController.swift b/packages/frontend/apps/ios/App/App/AFFiNEViewController.swift index fd08443972..0e25db8900 100644 --- a/packages/frontend/apps/ios/App/App/AFFiNEViewController.swift +++ b/packages/frontend/apps/ios/App/App/AFFiNEViewController.swift @@ -8,4 +8,8 @@ class AFFiNEViewController: CAPBridgeViewController { webView?.allowsBackForwardNavigationGestures = true } + override func capacitorDidLoad() { + bridge?.registerPluginInstance(CookiePlugin()) + } + } diff --git a/packages/frontend/apps/ios/App/App/Info.plist b/packages/frontend/apps/ios/App/App/Info.plist index e59b9163f5..10b7d08c8b 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/App/plugins/Cookie/CookieManager.swift b/packages/frontend/apps/ios/App/App/plugins/Cookie/CookieManager.swift new file mode 100644 index 0000000000..10538ca637 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/plugins/Cookie/CookieManager.swift @@ -0,0 +1,30 @@ +import Foundation +import UIKit + +public class CookieManager: NSObject { + public func getCookies(_ urlString: String) -> [String: String] { + var cookiesMap: [String: String] = [:] + let jar = HTTPCookieStorage.shared + guard let url = getServerUrl(urlString) else { return [:] } + if let cookies = jar.cookies(for: url) { + for cookie in cookies { + cookiesMap[cookie.name] = cookie.value + } + } + return cookiesMap + } + + private func isUrlSanitized(_ urlString: String) -> Bool { + return urlString.hasPrefix("http://") || urlString.hasPrefix("https://") + } + + public func getServerUrl(_ urlString: String) -> URL? { + let validUrlString = (isUrlSanitized(urlString)) ? urlString : "http://\(urlString)" + + guard let url = URL(string: validUrlString) else { + return nil + } + + return url + } +} diff --git a/packages/frontend/apps/ios/App/App/plugins/Cookie/CookiePlugin.swift b/packages/frontend/apps/ios/App/App/plugins/Cookie/CookiePlugin.swift new file mode 100644 index 0000000000..637414fb70 --- /dev/null +++ b/packages/frontend/apps/ios/App/App/plugins/Cookie/CookiePlugin.swift @@ -0,0 +1,21 @@ +import Foundation +import Capacitor + +@objc(CookiePlugin) +public class CookiePlugin: CAPPlugin, CAPBridgedPlugin { + public let identifier = "CookiePlugin" + public let jsName = "Cookie" + public let pluginMethods: [CAPPluginMethod] = [ + CAPPluginMethod(name: "getCookies", returnType: CAPPluginReturnPromise) + ] + + let cookieManager = CookieManager() + + @objc public func getCookies(_ call: CAPPluginCall) { + guard let url = call.getString("url") else { + return call.resolve([:]) + } + + call.resolve(cookieManager.getCookies(url)) + } +} diff --git a/packages/frontend/apps/ios/App/Podfile b/packages/frontend/apps/ios/App/Podfile index 42b9c05276..06b9627ac4 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 8d5b740a1c..f6a8f3ed5a 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 56eee290e6..236dd793ce 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 9f9fd1cbfa..de11ba96ec 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 4f88ff26cc..3dceaa3181 100644 --- a/packages/frontend/apps/ios/src/app.tsx +++ b/packages/frontend/apps/ios/src/app.tsx @@ -4,14 +4,21 @@ 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, WebSocketAuthProvider } from '@affine/core/modules/cloud'; import { I18nProvider } from '@affine/core/modules/i18n'; import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage'; +import { + ClientSchemaProvider, + PopupWindowProvider, +} from '@affine/core/modules/url'; import { configureIndexedDBUserspaceStorageProvider } from '@affine/core/modules/userspace'; import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench'; import { configureBrowserWorkspaceFlavours, configureIndexedDBWorkspaceEngineStorageProvider, } from '@affine/core/modules/workspace-engine'; +import { App as CapacitorApp } from '@capacitor/app'; +import { Browser } from '@capacitor/browser'; import { Framework, FrameworkRoot, @@ -21,6 +28,9 @@ import { import { Suspense } from 'react'; import { RouterProvider } from 'react-router-dom'; +import { configureFetchProvider } from './fetch'; +import { Cookie } from './plugins/cookie'; + const future = { v7_startTransition: true, } as const; @@ -33,6 +43,31 @@ configureBrowserWorkspaceFlavours(framework); configureIndexedDBWorkspaceEngineStorageProvider(framework); configureIndexedDBUserspaceStorageProvider(framework); configureMobileModules(framework); +framework.impl(PopupWindowProvider, { + open: (url: string) => { + Browser.open({ + url, + presentationStyle: 'popover', + }).catch(console.error); + }, +}); +framework.impl(ClientSchemaProvider, { + getClientSchema() { + return 'affine'; + }, +}); +configureFetchProvider(framework); +framework.impl(WebSocketAuthProvider, { + getAuthToken: async url => { + const cookies = await Cookie.getCookies({ + url, + }); + return { + userId: cookies['affine_user_id'], + token: cookies['affine_session'], + }; + }, +}); const frameworkProvider = framework.provider(); // setup application lifecycle events, and emit application start event @@ -41,6 +76,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/apps/ios/src/fetch.ts b/packages/frontend/apps/ios/src/fetch.ts new file mode 100644 index 0000000000..adf9e785bc --- /dev/null +++ b/packages/frontend/apps/ios/src/fetch.ts @@ -0,0 +1,194 @@ +/** + * this file is modified from part of https://github.com/ionic-team/capacitor/blob/74c3e9447e1e32e73f818d252eb12f453d849e8d/ios/Capacitor/Capacitor/assets/native-bridge.js#L466 + * + * for support arraybuffer response type + */ +import { FetchProvider } from '@affine/core/modules/cloud/provider/fetch'; +import { CapacitorHttp } from '@capacitor/core'; +import type { Framework } from '@toeverything/infra'; + +const readFileAsBase64 = (file: File) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + const data = reader.result; + if (data === null) { + reject(new Error('Failed to read file')); + } else { + resolve(btoa(data as string)); + } + }; + reader.onerror = reject; + reader.readAsBinaryString(file); + }); +const convertFormData = async (formData: FormData) => { + const newFormData = []; + // @ts-expect-error FormData.entries + for (const pair of formData.entries()) { + const [key, value] = pair; + if (value instanceof File) { + const base64File = await readFileAsBase64(value); + newFormData.push({ + key, + value: base64File, + type: 'base64File', + contentType: value.type, + fileName: value.name, + }); + } else { + newFormData.push({ key, value, type: 'string' }); + } + } + return newFormData; +}; +const convertBody = async (body: unknown, contentType: string) => { + if (body instanceof ReadableStream || body instanceof Uint8Array) { + let encodedData; + if (body instanceof ReadableStream) { + const reader = body.getReader(); + const chunks = []; + // eslint-disable-next-line no-constant-condition + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + const concatenated = new Uint8Array( + chunks.reduce((acc, chunk) => acc + chunk.length, 0) + ); + let position = 0; + for (const chunk of chunks) { + concatenated.set(chunk, position); + position += chunk.length; + } + encodedData = concatenated; + } else { + encodedData = body; + } + let data = new TextDecoder().decode(encodedData); + let type; + if (contentType === 'application/json') { + try { + data = JSON.parse(data); + } catch { + // ignore + } + type = 'json'; + } else if (contentType === 'multipart/form-data') { + type = 'formData'; + } else if ( + contentType === null || contentType === void 0 + ? void 0 + : contentType.startsWith('image') + ) { + type = 'image'; + } else if (contentType === 'application/octet-stream') { + type = 'binary'; + } else { + type = 'text'; + } + return { + data, + type, + headers: { 'Content-Type': contentType || 'application/octet-stream' }, + }; + } else if (body instanceof URLSearchParams) { + return { + data: body.toString(), + type: 'text', + }; + } else if (body instanceof FormData) { + const formData = await convertFormData(body); + return { + data: formData, + type: 'formData', + }; + } else if (body instanceof File) { + const fileData = await readFileAsBase64(body); + return { + data: fileData, + type: 'file', + headers: { 'Content-Type': body.type }, + }; + } + return { data: body, type: 'json' }; +}; +function base64ToUint8Array(base64: string) { + const binaryString = atob(base64); + const binaryArray = binaryString.split('').map(function (char) { + return char.charCodeAt(0); + }); + return new Uint8Array(binaryArray); +} +export function configureFetchProvider(framework: Framework) { + framework.override(FetchProvider, { + fetch: async (input, init) => { + const request = new Request(input, init); + const { method } = request; + const tag = `CapacitorHttp fetch ${Date.now()} ${input}`; + console.time(tag); + try { + const { body } = request; + // @ts-expect-error Headers.entries + const optionHeaders = Object.fromEntries(request.headers.entries()); + const { + data: requestData, + type, + headers, + } = await convertBody( + (init === null || init === void 0 ? void 0 : init.body) || + body || + undefined, + optionHeaders['Content-Type'] || optionHeaders['content-type'] + ); + const nativeResponse = await CapacitorHttp.request({ + url: request.url, + method: method, + data: requestData, + dataType: type as any, + responseType: + (optionHeaders['Accept'] || optionHeaders['accept']) === + 'application/octet-stream' + ? 'arraybuffer' + : undefined, + headers: Object.assign(Object.assign({}, headers), optionHeaders), + }); + const contentType = + nativeResponse.headers['Content-Type'] || + nativeResponse.headers['content-type']; + let data = ( + contentType === null || contentType === void 0 + ? void 0 + : contentType.startsWith('application/json') + ) + ? JSON.stringify(nativeResponse.data) + : contentType === 'application/octet-stream' + ? base64ToUint8Array(nativeResponse.data) + : nativeResponse.data; + + // use null data for 204 No Content HTTP response + if (nativeResponse.status === 204) { + data = null; + } + // intercept & parse response before returning + const response = new Response(data, { + headers: nativeResponse.headers, + status: nativeResponse.status, + }); + /* + * copy url to response, `cordova-plugin-ionic` uses this url from the response + * we need `Object.defineProperty` because url is an inherited getter on the Response + * see: https://stackoverflow.com/a/57382543 + * */ + Object.defineProperty(response, 'url', { + value: nativeResponse.url, + }); + console.timeEnd(tag); + return response; + } catch (error) { + console.timeEnd(tag); + throw error; + } + }, + }); +} diff --git a/packages/frontend/apps/ios/src/index.tsx b/packages/frontend/apps/ios/src/index.tsx index 4d279a07a5..70470613e1 100644 --- a/packages/frontend/apps/ios/src/index.tsx +++ b/packages/frontend/apps/ios/src/index.tsx @@ -18,6 +18,15 @@ import { App } from './app'; function main() { if (BUILD_CONFIG.debug || window.SENTRY_RELEASE) { + // workaround for Capacitor HttpPlugin + // capacitor-http-plugin will replace window.XMLHttpRequest with its own implementation + // but XMLHttpRequest.prototype is not defined which is used by sentry + // see: https://github.com/ionic-team/capacitor/blob/74c3e9447e1e32e73f818d252eb12f453d849e8d/core/native-bridge.ts#L581 + if ('CapacitorWebXMLHttpRequest' in window) { + window.XMLHttpRequest.prototype = ( + window.CapacitorWebXMLHttpRequest as any + ).prototype; + } // https://docs.sentry.io/platforms/javascript/guides/react/#configure init({ dsn: process.env.SENTRY_DSN, diff --git a/packages/frontend/apps/ios/src/plugins/cookie/definitions.ts b/packages/frontend/apps/ios/src/plugins/cookie/definitions.ts new file mode 100644 index 0000000000..125c661c1c --- /dev/null +++ b/packages/frontend/apps/ios/src/plugins/cookie/definitions.ts @@ -0,0 +1,6 @@ +export interface CookiePlugin { + /** + * Returns the screen's current orientation. + */ + getCookies(options: { url: string }): Promise>; +} diff --git a/packages/frontend/apps/ios/src/plugins/cookie/index.ts b/packages/frontend/apps/ios/src/plugins/cookie/index.ts new file mode 100644 index 0000000000..e156c0de47 --- /dev/null +++ b/packages/frontend/apps/ios/src/plugins/cookie/index.ts @@ -0,0 +1,8 @@ +import { registerPlugin } from '@capacitor/core'; + +import type { CookiePlugin } from './definitions'; + +const Cookie = registerPlugin('Cookie'); + +export * from './definitions'; +export { Cookie }; diff --git a/packages/frontend/apps/mobile/src/app.tsx b/packages/frontend/apps/mobile/src/app.tsx index 4f88ff26cc..b9fcfafcb2 100644 --- a/packages/frontend/apps/mobile/src/app.tsx +++ b/packages/frontend/apps/mobile/src/app.tsx @@ -6,6 +6,7 @@ import { router } from '@affine/core/mobile/router'; import { configureCommonModules } from '@affine/core/modules'; import { I18nProvider } from '@affine/core/modules/i18n'; import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage'; +import { PopupWindowProvider } from '@affine/core/modules/url'; import { configureIndexedDBUserspaceStorageProvider } from '@affine/core/modules/userspace'; import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench'; import { @@ -33,6 +34,25 @@ configureBrowserWorkspaceFlavours(framework); configureIndexedDBWorkspaceEngineStorageProvider(framework); configureIndexedDBUserspaceStorageProvider(framework); configureMobileModules(framework); +framework.impl(PopupWindowProvider, { + open: (target: string) => { + const targetUrl = new URL(target); + + let url: string; + // safe to open directly if in the same origin + if (targetUrl.origin === location.origin) { + url = target; + } else { + const redirectProxy = location.origin + '/redirect-proxy'; + const search = new URLSearchParams({ + redirect_uri: target, + }); + + url = `${redirectProxy}?${search.toString()}`; + } + window.open(url, '_blank', 'noreferrer noopener'); + }, +}); const frameworkProvider = framework.provider(); // setup application lifecycle events, and emit application start event diff --git a/packages/frontend/apps/web/src/app.tsx b/packages/frontend/apps/web/src/app.tsx index daf97ccd3c..0ad58c379d 100644 --- a/packages/frontend/apps/web/src/app.tsx +++ b/packages/frontend/apps/web/src/app.tsx @@ -7,6 +7,7 @@ import { configureCommonModules } from '@affine/core/modules'; import { I18nProvider } from '@affine/core/modules/i18n'; import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage'; import { CustomThemeModifier } from '@affine/core/modules/theme-editor'; +import { PopupWindowProvider } from '@affine/core/modules/url'; import { configureIndexedDBUserspaceStorageProvider } from '@affine/core/modules/userspace'; import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench'; import { @@ -37,6 +38,25 @@ configureLocalStorageStateStorageImpls(framework); configureBrowserWorkspaceFlavours(framework); configureIndexedDBWorkspaceEngineStorageProvider(framework); configureIndexedDBUserspaceStorageProvider(framework); +framework.impl(PopupWindowProvider, { + open: (target: string) => { + const targetUrl = new URL(target); + + let url: string; + // safe to open directly if in the same origin + if (targetUrl.origin === location.origin) { + url = target; + } else { + const redirectProxy = location.origin + '/redirect-proxy'; + const search = new URLSearchParams({ + redirect_uri: target, + }); + + url = `${redirectProxy}?${search.toString()}`; + } + window.open(url, '_blank', 'noreferrer noopener'); + }, +}); const frameworkProvider = framework.provider(); // setup application lifecycle events, and emit application start event diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json index d7f4fe2b3e..ac28113a0d 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/commands/affine-help.tsx b/packages/frontend/core/src/commands/affine-help.tsx index f99695c4e5..1486c050d9 100644 --- a/packages/frontend/core/src/commands/affine-help.tsx +++ b/packages/frontend/core/src/commands/affine-help.tsx @@ -4,15 +4,17 @@ import { ContactWithUsIcon, NewIcon } from '@blocksuite/icons/rc'; import type { createStore } from 'jotai'; import { openSettingModalAtom } from '../components/atoms'; -import { popupWindow } from '../utils'; +import type { UrlService } from '../modules/url'; import { registerAffineCommand } from './registry'; export function registerAffineHelpCommands({ t, store, + urlService, }: { t: ReturnType; store: ReturnType; + urlService: UrlService; }) { const unsubs: Array<() => void> = []; unsubs.push( @@ -23,7 +25,7 @@ export function registerAffineHelpCommands({ label: t['com.affine.cmdk.affine.whats-new'](), run() { track.$.cmdk.help.openChangelog(); - popupWindow(BUILD_CONFIG.changelogUrl); + urlService.openPopupWindow(BUILD_CONFIG.changelogUrl); }, }) ); diff --git a/packages/frontend/core/src/components/affine/auth/oauth.tsx b/packages/frontend/core/src/components/affine/auth/oauth.tsx index f464e160ed..01d61128f7 100644 --- a/packages/frontend/core/src/components/affine/auth/oauth.tsx +++ b/packages/frontend/core/src/components/affine/auth/oauth.tsx @@ -1,7 +1,6 @@ import { Skeleton } from '@affine/component'; import { Button } from '@affine/component/ui/button'; -import { popupWindow } from '@affine/core/utils'; -import { appInfo } from '@affine/electron-api'; +import { UrlService } from '@affine/core/modules/url'; import { OAuthProviderType } from '@affine/graphql'; import { GithubIcon, GoogleDuotoneIcon } from '@blocksuite/icons/rc'; import { useLiveData, useService } from '@toeverything/infra'; @@ -31,10 +30,12 @@ const OAuthProviderMap: Record< export function OAuth({ redirectUrl }: { redirectUrl?: string }) { const serverConfig = useService(ServerConfigService).serverConfig; + const urlService = useService(UrlService); const oauth = useLiveData(serverConfig.features$.map(r => r?.oauth)); const oauthProviders = useLiveData( serverConfig.config$.map(r => r?.oauthProviders) ); + const schema = urlService.getClientSchema(); if (!oauth) { return ; @@ -45,6 +46,10 @@ export function OAuth({ redirectUrl }: { redirectUrl?: string }) { key={provider} provider={provider} redirectUrl={redirectUrl} + schema={schema} + popupWindow={url => { + urlService.openPopupWindow(url); + }} /> )); } @@ -52,9 +57,13 @@ export function OAuth({ redirectUrl }: { redirectUrl?: string }) { function OAuthProvider({ provider, redirectUrl, + schema, + popupWindow, }: { provider: OAuthProviderType; redirectUrl?: string; + schema?: string; + popupWindow: (url: string) => void; }) { const { icon } = OAuthProviderMap[provider]; @@ -67,17 +76,18 @@ function OAuthProvider({ params.set('redirect_uri', redirectUrl); } - if (BUILD_CONFIG.isElectron && appInfo) { - params.set('client', appInfo.schema); + if (schema) { + params.set('client', schema); } + // TODO: Android app scheme not implemented + // if (BUILD_CONFIG.isAndroid) {} + const oauthUrl = - (BUILD_CONFIG.isElectron || BUILD_CONFIG.isIOS || BUILD_CONFIG.isAndroid - ? BUILD_CONFIG.serverUrlPrefix - : '') + `/oauth/login?${params.toString()}`; + BUILD_CONFIG.serverUrlPrefix + `/oauth/login?${params.toString()}`; popupWindow(oauthUrl); - }, [provider, redirectUrl]); + }, [popupWindow, provider, redirectUrl, schema]); return (