diff --git a/.eslintignore b/.eslintignore index e136a6b38..d6ceb5dc0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -191,6 +191,7 @@ packages/app-desktop/gui/IconButton.js packages/app-desktop/gui/ImportScreen.js packages/app-desktop/gui/ItemList.js packages/app-desktop/gui/JoplinCloudConfigScreen.js +packages/app-desktop/gui/JoplinCloudLoginScreen.js packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js packages/app-desktop/gui/KeymapConfig/styles/index.js @@ -884,6 +885,7 @@ packages/lib/services/interop/InteropService_Importer_Raw.js packages/lib/services/interop/Module.test.js packages/lib/services/interop/Module.js packages/lib/services/interop/types.js +packages/lib/services/joplinCloudUtils.js packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js packages/lib/services/keychain/KeychainService.js packages/lib/services/keychain/KeychainServiceDriver.dummy.js diff --git a/.gitignore b/.gitignore index f83699def..89dbf35c0 100644 --- a/.gitignore +++ b/.gitignore @@ -171,6 +171,7 @@ packages/app-desktop/gui/IconButton.js packages/app-desktop/gui/ImportScreen.js packages/app-desktop/gui/ItemList.js packages/app-desktop/gui/JoplinCloudConfigScreen.js +packages/app-desktop/gui/JoplinCloudLoginScreen.js packages/app-desktop/gui/KeymapConfig/KeymapConfigScreen.js packages/app-desktop/gui/KeymapConfig/ShortcutRecorder.js packages/app-desktop/gui/KeymapConfig/styles/index.js @@ -864,6 +865,7 @@ packages/lib/services/interop/InteropService_Importer_Raw.js packages/lib/services/interop/Module.test.js packages/lib/services/interop/Module.js packages/lib/services/interop/types.js +packages/lib/services/joplinCloudUtils.js packages/lib/services/joplinServer/personalizedUserContentBaseUrl.js packages/lib/services/keychain/KeychainService.js packages/lib/services/keychain/KeychainServiceDriver.dummy.js diff --git a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx index 3493abee3..65cedf135 100644 --- a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx @@ -58,6 +58,15 @@ class ConfigScreenComponent extends React.Component { } private async checkSyncConfig_() { + if (this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud')) { + const isAuthenticated = await reg.syncTarget().isAuthenticated(); + if (!isAuthenticated) { + return this.props.dispatch({ + type: 'NAV_GO', + routeName: 'JoplinCloudLogin', + }); + } + } await shared.checkSyncConfig(this, this.state.settings); } diff --git a/packages/app-desktop/gui/JoplinCloudLoginScreen.scss b/packages/app-desktop/gui/JoplinCloudLoginScreen.scss new file mode 100644 index 000000000..88583dfb5 --- /dev/null +++ b/packages/app-desktop/gui/JoplinCloudLoginScreen.scss @@ -0,0 +1,32 @@ +.login-page { + + display: flex; + flex-direction: column; + height: 100%; + background-color: var(--joplin-background-color); + color: var(--joplin-color); + + > .page-container { + height: calc(100% - (var(--joplin-margin) * 2)); + flex: 1; + padding: var(--joplin-config-screen-padding); + + > .text { + font-size: var(--joplin-font-size); + } + + > .buttons-container { + margin-bottom: calc(var(--joplin-font-size) * 2); + display: flex; + + > button:first-child { + margin-right: calc(var(--joplin-font-size) * 2); + } + } + + > .bold { + font-weight: bold; + font-size: calc(var(--joplin-font-size) * 1.3); + } + } +} \ No newline at end of file diff --git a/packages/app-desktop/gui/JoplinCloudLoginScreen.tsx b/packages/app-desktop/gui/JoplinCloudLoginScreen.tsx new file mode 100644 index 000000000..09a4c359b --- /dev/null +++ b/packages/app-desktop/gui/JoplinCloudLoginScreen.tsx @@ -0,0 +1,115 @@ +import { useEffect, useMemo, useReducer, useState } from 'react'; +import ButtonBar from './ConfigScreen/ButtonBar'; +import { _ } from '@joplin/lib/locale'; +import { clipboard } from 'electron'; +import Button, { ButtonLevel } from './Button/Button'; +const bridge = require('@electron/remote').require('./bridge').default; +import { uuidgen } from '@joplin/lib/uuid'; +import { Dispatch } from 'redux'; +import { reducer, defaultState, generateApplicationConfirmUrl, checkIfLoginWasSuccessful } from '@joplin/lib/services/joplinCloudUtils'; +import { AppState } from '../app.reducer'; +import Logger from '@joplin/utils/Logger'; + +const logger = Logger.create('JoplinCloudLoginScreen'); +const { connect } = require('react-redux'); + +interface Props { + dispatch: Dispatch; + joplinCloudWebsite: string; + joplinCloudApi: string; +} + +const JoplinCloudScreenComponent = (props: Props) => { + + const confirmUrl = (applicationAuthId: string) => `${props.joplinCloudWebsite}/applications/${applicationAuthId}/confirm`; + const applicationAuthUrl = (applicationAuthId: string) => `${props.joplinCloudApi}/api/application_auth/${applicationAuthId}`; + + const [intervalIdentifier, setIntervalIdentifier] = useState(undefined); + const [state, dispatch] = useReducer(reducer, defaultState); + + const applicatioAuthId = useMemo(() => uuidgen(), []); + + const periodicallyCheckForCredentials = () => { + if (intervalIdentifier) return; + + const interval = setInterval(async () => { + try { + const response = await checkIfLoginWasSuccessful(applicationAuthUrl(applicatioAuthId)); + if (response && response.success) { + dispatch({ type: 'COMPLETED' }); + clearInterval(interval); + } + } catch (error) { + logger.error(error); + dispatch({ type: 'ERROR', payload: error.message }); + clearInterval(interval); + } + }, 2 * 1000); + + setIntervalIdentifier(interval); + }; + + const onButtonUsed = () => { + if (state.next === 'LINK_USED') { + dispatch({ type: 'LINK_USED' }); + } + periodicallyCheckForCredentials(); + }; + + const onAuthorizeClicked = async () => { + const url = await generateApplicationConfirmUrl(confirmUrl(applicatioAuthId)); + bridge().openExternal(url); + onButtonUsed(); + }; + + const onCopyToClipboardClicked = async () => { + const url = await generateApplicationConfirmUrl(confirmUrl(applicatioAuthId)); + clipboard.writeText(url); + onButtonUsed(); + }; + + useEffect(() => { + return () => { + clearInterval(intervalIdentifier); + }; + }, [intervalIdentifier]); + + return ( +
+
+

{_('To allow Joplin to synchronise with Joplin Cloud, open this URL in your browser to authorise the application:')}

+
+
+

{state.message()} + {state.active === 'ERROR' ? ( + {state.errorMessage} + ) : null} +

+ {state.active === 'LINK_USED' ?
: null} +
+ props.dispatch({ type: 'NAV_BACK' })} /> +
+ ); +}; + +const mapStateToProps = (state: AppState) => { + return { + joplinCloudWebsite: state.settings['sync.10.website'], + joplinCloudApi: state.settings['sync.10.path'], + }; +}; + +export default connect(mapStateToProps)(JoplinCloudScreenComponent); diff --git a/packages/app-desktop/gui/Root.tsx b/packages/app-desktop/gui/Root.tsx index da1e4c061..ed3d7d110 100644 --- a/packages/app-desktop/gui/Root.tsx +++ b/packages/app-desktop/gui/Root.tsx @@ -28,6 +28,7 @@ import ImportScreen from './ImportScreen'; const { ResourceScreen } = require('./ResourceScreen.js'); import Navigator from './Navigator'; import WelcomeUtils from '@joplin/lib/WelcomeUtils'; +import JoplinCloudLoginScreen from './JoplinCloudLoginScreen'; const { ThemeProvider, StyleSheetManager, createGlobalStyle } = require('styled-components'); const bridge = require('@electron/remote').require('./bridge').default; @@ -224,6 +225,7 @@ class RootComponent extends React.Component { Main: { screen: MainScreen }, OneDriveLogin: { screen: OneDriveLoginScreen, title: () => _('OneDrive Login') }, DropboxLogin: { screen: DropboxLoginScreen, title: () => _('Dropbox Login') }, + JoplinCloudLogin: { screen: JoplinCloudLoginScreen, title: () => _('Joplin Cloud Login') }, Import: { screen: ImportScreen, title: () => _('Import') }, Config: { screen: ConfigScreen, title: () => _('Options') }, Resources: { screen: ResourceScreen, title: () => _('Note attachments') }, diff --git a/packages/app-desktop/gui/SyncWizard/Dialog.tsx b/packages/app-desktop/gui/SyncWizard/Dialog.tsx index d9ec4c118..f1e0530c6 100644 --- a/packages/app-desktop/gui/SyncWizard/Dialog.tsx +++ b/packages/app-desktop/gui/SyncWizard/Dialog.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useState, useRef, useCallback } from 'react'; +import { useRef, useCallback } from 'react'; import { _ } from '@joplin/lib/locale'; import DialogButtonRow from '../DialogButtonRow'; import Dialog from '../Dialog'; @@ -9,10 +9,7 @@ import SyncTargetRegistry, { SyncTargetInfo } from '@joplin/lib/SyncTargetRegist import useElementSize from '@joplin/lib/hooks/useElementSize'; import Button, { ButtonLevel } from '../Button/Button'; import bridge from '../../services/bridge'; -import StyledInput from '../style/StyledInput'; import Setting from '@joplin/lib/models/Setting'; -import SyncTargetJoplinCloud from '@joplin/lib/SyncTargetJoplinCloud'; -import StyledLink from '../style/StyledLink'; interface Props { themeId: number; @@ -32,10 +29,6 @@ const SyncTargetDescription = styled.div<{ height: number }>` font-size: 16px; `; -const CreateAccountLink = styled(StyledLink)` - font-size: 16px; -`; - const ContentRoot = styled.div` background-color: ${props => props.theme.backgroundColor3}; padding: 1em; @@ -70,7 +63,7 @@ const SyncTargetLogo = styled.img` margin-right: 0.4em; `; -const SyncTargetBox = styled.div<{ faded: boolean }>` +const SyncTargetBox = styled.div` display: flex; flex: 1; flex-direction: column; @@ -82,7 +75,7 @@ const SyncTargetBox = styled.div<{ faded: boolean }>` padding: 2em 2.2em 2em 2.2em; margin-right: 1em; max-width: 400px; - opacity: ${props => props.faded ? 0.5 : 1}; + opacity: 1; `; const FeatureList = styled.div` @@ -117,16 +110,6 @@ const SelectButton = styled(Button)` font-size: 1em; `; -const JoplinCloudLoginForm = styled.div` - display: flex; - flex-direction: column; -`; - -const FormLabel = styled.label` - font-weight: bold; - margin: 1em 0 0.6em 0; -`; - const SlowSyncWarning = styled.div` margin-top: 1em; opacity: 0.8; @@ -152,12 +135,10 @@ const logosImageNames: Record = { 'onedrive': 'SyncTarget_OneDrive.svg', }; +type SyncTargetInfoName = 'dropbox' | 'onedrive' | 'joplinCloud'; + export default function(props: Props) { - const [showJoplinCloudForm, setShowJoplinCloudForm] = useState(false); const joplinCloudDescriptionRef = useRef(null); - const [joplinCloudEmail, setJoplinCloudEmail] = useState(''); - const [joplinCloudPassword, setJoplinCloudPassword] = useState(''); - const [joplinCloudLoginInProgress, setJoplinCloudLoginInProgress] = useState(false); // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied function closeDialog(dispatch: Function) { @@ -192,93 +173,33 @@ export default function(props: Props) { ); } - const onJoplinCloudEmailChange = useCallback((event: any) => { - setJoplinCloudEmail(event.target.value); - }, []); + const onSelectButtonClick = useCallback(async (name: SyncTargetInfoName) => { + const routes = { + 'dropbox': { name: 'DropboxLogin', target: 7 }, + 'onedrive': { name: 'OneDriveLogin', target: 3 }, + 'joplinCloud': { name: 'JoplinCloudLogin', target: 10 }, + }; + const route = routes[name]; + if (!route) return; // throw error?? - const onJoplinCloudPasswordChange = useCallback((event: any) => { - setJoplinCloudPassword(event.target.value); - }, []); - - const onJoplinCloudLoginClick = useCallback(async () => { - setJoplinCloudLoginInProgress(true); - - let result = null; - - try { - result = await SyncTargetJoplinCloud.checkConfig({ - password: () => joplinCloudPassword, - path: () => Setting.value('sync.10.path'), - userContentPath: () => Setting.value('sync.10.userContentPath'), - username: () => joplinCloudEmail, - }); - } finally { - setJoplinCloudLoginInProgress(false); - } - - if (result.ok) { - Setting.setValue('sync.target', 10); - Setting.setValue('sync.10.username', joplinCloudEmail); - Setting.setValue('sync.10.password', joplinCloudPassword); - await Setting.saveAll(); - - alert(_('Thank you! Your Joplin Cloud account is now setup and ready to use.')); - - closeDialog(props.dispatch); - - props.dispatch({ - type: 'NAV_GO', - routeName: 'Main', - }); - } else { - alert(_('There was an error setting up your Joplin Cloud account. Please verify your email and password and try again. Error was:\n\n%s', result.errorMessage)); - } - }, [joplinCloudEmail, joplinCloudPassword, props.dispatch]); - - const onJoplinCloudCreateAccountClick = useCallback(() => { - void bridge().openExternal('https://joplinapp.org/plans/'); - }, []); - - function renderJoplinCloudLoginForm() { - return ( - -
{_('Login below.')} {_('Or create an account.')}
- {_('Email')} - - {_('Password')} - - -
- ); - } - - const onSelectButtonClick = useCallback(async (name: string) => { - if (name === 'joplinCloud') { - setShowJoplinCloudForm(true); - } else { - Setting.setValue('sync.target', name === 'dropbox' ? 7 : 3); - await Setting.saveAll(); - closeDialog(props.dispatch); - props.dispatch({ - type: 'NAV_GO', - routeName: name === 'dropbox' ? 'DropboxLogin' : 'OneDriveLogin', - }); - } + Setting.setValue('sync.target', route.target); + await Setting.saveAll(); + closeDialog(props.dispatch); + props.dispatch({ + type: 'NAV_GO', + routeName: route.name, + }); }, [props.dispatch]); function renderSelectArea(info: SyncTargetInfo) { - if (info.name === 'joplinCloud' && showJoplinCloudForm) { - return renderJoplinCloudLoginForm(); - } else { - return ( - onSelectButtonClick(info.name)} - disabled={joplinCloudLoginInProgress} - /> - ); - } + return ( + onSelectButtonClick(info.name as SyncTargetInfoName)} + disabled={false} + /> + ); } function renderSyncTarget(info: SyncTargetInfo) { @@ -289,7 +210,7 @@ export default function(props: Props) { const logoImageSrc = logoImageName ? `${bridge().buildDir()}/images/${logoImageName}` : ''; const logo = logoImageSrc ? : null; const descriptionComp = {info.description}; - const featuresComp = showJoplinCloudForm && info.name === 'joplinCloud' ? null : renderFeatures(info.name); + const featuresComp = renderFeatures(info.name); const renderSlowSyncWarning = () => { if (info.name === 'joplinCloud') return null; @@ -297,7 +218,7 @@ export default function(props: Props) { }; return ( - + {logo}{info.label} {descriptionComp} {featuresComp} @@ -328,7 +249,7 @@ export default function(props: Props) { boxes.push(renderSyncTarget(info)); } - const selfHostingMessage = showJoplinCloudForm ? null : Self-hosting? Joplin also supports various self-hosting options such as Nextcloud, WebDAV, AWS S3 and Joplin Server. Click here to select one.; + const selfHostingMessage = Self-hosting? Joplin also supports various self-hosting options such as Nextcloud, WebDAV, AWS S3 and Joplin Server. Click here to select one.; return ( diff --git a/packages/app-desktop/main.scss b/packages/app-desktop/main.scss index 15bc2d860..3fa523a43 100644 --- a/packages/app-desktop/main.scss +++ b/packages/app-desktop/main.scss @@ -122,7 +122,7 @@ a { margin: 40px 20px; } -.modal-message #loading-animation { +#loading-animation { margin-right: 20px; width: 20px; height: 20px; diff --git a/packages/app-desktop/style.scss b/packages/app-desktop/style.scss index 9caff7406..2cf9a8e83 100644 --- a/packages/app-desktop/style.scss +++ b/packages/app-desktop/style.scss @@ -6,6 +6,7 @@ @use 'gui/Dropdown/style.scss' as dropdown-control; @use 'gui/ShareFolderDialog/style.scss' as share-folder-dialog; @use 'gui/NoteList/style.scss' as note-list; +@use 'gui/JoplinCloudLoginScreen.scss' as joplin-cloud-login-screen; @use 'gui/NoteListHeader/style.scss' as note-list-header; @use 'gui/TrashNotification/style.scss' as trash-notification; @use 'main.scss' as main; \ No newline at end of file diff --git a/packages/lib/BaseApplication.ts b/packages/lib/BaseApplication.ts index 3387353a6..ce4477b44 100644 --- a/packages/lib/BaseApplication.ts +++ b/packages/lib/BaseApplication.ts @@ -777,6 +777,7 @@ export default class BaseApplication { // Setting.setValue('sync.10.userContentPath', 'https://joplinusercontent.com'); Setting.setValue('sync.10.path', 'http://api.joplincloud.local:22300'); Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300'); + Setting.setValue('sync.10.website', 'http://joplincloud.local:22300'); } // For now always disable fuzzy search due to performance issues: diff --git a/packages/lib/JoplinServerApi.ts b/packages/lib/JoplinServerApi.ts index 5bf29e310..9f9675142 100644 --- a/packages/lib/JoplinServerApi.ts +++ b/packages/lib/JoplinServerApi.ts @@ -6,6 +6,7 @@ import { Env } from './models/Setting'; import Logger from '@joplin/utils/Logger'; import personalizedUserContentBaseUrl from './services/joplinServer/personalizedUserContentBaseUrl'; import { getHttpStatusMessage } from './net-utils'; +import { getApplicationInformation } from './services/joplinCloudUtils'; const { stringify } = require('query-string'); const logger = Logger.create('JoplinServerApi'); @@ -63,13 +64,27 @@ export default class JoplinServerApi { return personalizedUserContentBaseUrl(userId, this.baseUrl(), this.options_.userContentBaseUrl()); } + private async getClientInfo() { + const { platform, type } = await getApplicationInformation(); + const clientInfo = { + platform, + type, + version: shim.appVersion(), + }; + + return clientInfo; + } + private async session() { if (this.session_) return this.session_; + const clientInfo = await this.getClientInfo(); + try { this.session_ = await this.exec_('POST', 'api/sessions', null, { email: this.options_.username(), password: this.options_.password(), + ...clientInfo, }); return this.session_; diff --git a/packages/lib/SyncTargetJoplinCloud.ts b/packages/lib/SyncTargetJoplinCloud.ts index 3734d1cca..cc4e89495 100644 --- a/packages/lib/SyncTargetJoplinCloud.ts +++ b/packages/lib/SyncTargetJoplinCloud.ts @@ -46,7 +46,19 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget { } public async isAuthenticated() { - return true; + try { + const fileApi = await this.fileApi(); + const api = fileApi.driver().api(); + const sessionId = await api.sessionId(); + return !!sessionId; + } catch (error) { + if (error.code === 403) return false; + throw error; + } + } + + public authRouteName() { + return 'JoplinCloudLogin'; } public static requiresPassword() { diff --git a/packages/lib/models/Setting.ts b/packages/lib/models/Setting.ts index 87e0163ec..e8bc989b3 100644 --- a/packages/lib/models/Setting.ts +++ b/packages/lib/models/Setting.ts @@ -705,26 +705,22 @@ class Setting extends BaseModel { public: false, storage: SettingStorage.Database, }, + 'sync.10.website': { + value: 'https://joplincloud.com', + type: SettingItemType.String, + public: false, + storage: SettingStorage.Database, + }, 'sync.10.username': { value: '', type: SettingItemType.String, - section: 'sync', - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud'); - }, - public: true, - label: () => _('Joplin Cloud email'), + public: false, storage: SettingStorage.File, }, 'sync.10.password': { value: '', type: SettingItemType.String, - section: 'sync', - show: (settings: any) => { - return settings['sync.target'] === SyncTargetRegistry.nameToId('joplinCloud'); - }, - public: true, - label: () => _('Joplin Cloud password'), + public: false, secure: true, }, diff --git a/packages/lib/services/joplinCloudUtils.ts b/packages/lib/services/joplinCloudUtils.ts new file mode 100644 index 000000000..8f102ca2b --- /dev/null +++ b/packages/lib/services/joplinCloudUtils.ts @@ -0,0 +1,114 @@ +import { Reducer } from 'react'; +import Setting from '../models/Setting'; +import { ApplicationPlatform, ApplicationType } from '../types'; +import shim from '../shim'; +import { _ } from '../locale'; + +type ActionType = 'LINK_USED' | 'COMPLETED' | 'ERROR'; +type Action = { + type: ActionType; + payload?: any; +}; + +type DefaultState = { + className: 'text' | 'bold'; + message: ()=> string; + next: ActionType; + active: ActionType | 'INITIAL'; + errorMessage?: string; +}; + +export const defaultState: DefaultState = { + className: 'text', + message: ()=> _('Waiting for authorisation...'), + next: 'LINK_USED', + active: 'INITIAL', +}; + +export const reducer: Reducer = (state: DefaultState, action: Action) => { + switch (action.type) { + case 'LINK_USED': { + return { + className: 'text', + message: () => _('If you have already authorised, please wait for the application to sync to Joplin Cloud.'), + next: 'COMPLETED', + active: 'LINK_USED', + }; + } + case 'COMPLETED': { + return { + className: 'bold', + message: () => _('You are logged in into Joplin Cloud, you can leave this screen now.'), + active: 'COMPLETED', + next: 'COMPLETED', + }; + } + case 'ERROR': { + return { + className: 'text', + message: () => _('You were unable to connect to Joplin Cloud, verify your connection. Error: '), + active: 'ERROR', + next: 'COMPLETED', + errorMessage: action.payload, + }; + } + default: { + return state; + } + } +}; + +export const getApplicationInformation = async () => { + const platformName = await shim.platformName(); + switch (platformName) { + case 'ios': + return { type: ApplicationType.Mobile, platform: ApplicationPlatform.Ios }; + case 'android': + return { type: ApplicationType.Mobile, platform: ApplicationPlatform.Android }; + case 'darwin': + return { type: ApplicationType.Desktop, platform: ApplicationPlatform.MacOs }; + case 'win32': + return { type: ApplicationType.Desktop, platform: ApplicationPlatform.Windows }; + case 'linux': + return { type: ApplicationType.Desktop, platform: ApplicationPlatform.Linux }; + default: + return { type: ApplicationType.Unknown, platform: ApplicationPlatform.Unknown }; + } +}; + +export const generateApplicationConfirmUrl = async (confirmUrl: string) => { + const applicationInfo = await getApplicationInformation(); + const searchParams = new URLSearchParams(); + searchParams.append('platform', applicationInfo.platform.toString()); + searchParams.append('type', applicationInfo.type.toString()); + searchParams.append('version', shim.appVersion()); + + return `${confirmUrl}?${searchParams.toString()}`; +}; + + +// We have isWaitingResponse inside the function to avoid any state from lingering +// after an error occurs. E.g.: if the function would throw an error while isWaitingResponse +// was set to true the next time we call the function the value would still be true. +// The closure function prevents that. +export const checkIfLoginWasSuccessful = async (applicationsUrl: string) => { + let isWaitingResponse = false; + const performLoginRequest = async () => { + if (isWaitingResponse) return undefined; + isWaitingResponse = true; + + const response = await fetch(applicationsUrl); + const jsonBody = await response.json(); + + if (!response.ok || jsonBody.status !== 'finished') { + isWaitingResponse = false; + return undefined; + } + + Setting.setValue('sync.10.username', jsonBody.id); + Setting.setValue('sync.10.password', jsonBody.password); + return { success: true }; + }; + + return performLoginRequest(); +}; diff --git a/packages/lib/types.ts b/packages/lib/types.ts index 38ac4de58..1a8338fdf 100644 --- a/packages/lib/types.ts +++ b/packages/lib/types.ts @@ -4,6 +4,7 @@ export enum ApplicationPlatform { Linux = 2, MacOs = 3, Android = 4, + Ios = 5, } export enum ApplicationType {