From a07453249755ebe88c0bf9c862de452c64dd4255 Mon Sep 17 00:00:00 2001 From: pedr Date: Mon, 1 Jul 2024 12:21:17 -0300 Subject: [PATCH] Desktop, Mobile: Fixes #10645: Show notification in case Joplin Cloud credential is not valid anymore (#10649) --- packages/app-desktop/app.ts | 1 + .../app-desktop/gui/MainScreen/MainScreen.tsx | 18 +++++++++++++++++- .../ScreenHeader/WarningBanner.test.tsx | 2 ++ .../components/ScreenHeader/WarningBanner.tsx | 5 +++++ packages/app-mobile/root.tsx | 5 +++-- packages/lib/SyncTargetJoplinCloud.ts | 8 ++++++-- packages/lib/Synchronizer.ts | 3 +++ .../shouldShowMissingPasswordWarning.test.ts | 2 +- packages/lib/reducer.ts | 6 ++++++ packages/lib/registry.ts | 18 ++++++++++++++++++ 10 files changed, 62 insertions(+), 6 deletions(-) diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index d49ca86e1..c0503faee 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -420,6 +420,7 @@ class Application extends BaseApplication { AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId })); AlarmService.setLogger(reg.logger()); + reg.setDispatch(this.dispatch.bind(this)); reg.setShowErrorMessageBoxHandler((message: string) => { bridge().showErrorMessageBox(message); }); if (Setting.value('flagOpenDevTools')) { diff --git a/packages/app-desktop/gui/MainScreen/MainScreen.tsx b/packages/app-desktop/gui/MainScreen/MainScreen.tsx index e97e0df6c..1e4de49e0 100644 --- a/packages/app-desktop/gui/MainScreen/MainScreen.tsx +++ b/packages/app-desktop/gui/MainScreen/MainScreen.tsx @@ -97,6 +97,7 @@ interface Props { notesSortOrderField: string; notesSortOrderReverse: boolean; notesColumns: NoteListColumns; + showInvalidJoplinCloudCredential: boolean; } interface ShareFolderDialogOptions { @@ -592,6 +593,13 @@ class MainScreenComponent extends React.Component { }); }; + const onViewJoplinCloudLoginScreen = () => { + this.props.dispatch({ + type: 'NAV_GO', + routeName: 'JoplinCloudLogin', + }); + }; + const onViewSyncSettingsScreen = () => { this.props.dispatch({ type: 'NAV_GO', @@ -684,6 +692,12 @@ class MainScreenComponent extends React.Component { ); } else if (this.props.mustUpgradeAppMessage) { msg = this.renderNotificationMessage(this.props.mustUpgradeAppMessage); + } else if (this.props.showInvalidJoplinCloudCredential) { + msg = this.renderNotificationMessage( + _('Your Joplin Cloud credentials are invalid, please login.'), + _('Login to Joplin Cloud.'), + onViewJoplinCloudLoginScreen, + ); } return ( @@ -705,7 +719,8 @@ class MainScreenComponent extends React.Component { props.isSafeMode || this.showShareInvitationNotification(props) || this.props.needApiAuth || - !!this.props.mustUpgradeAppMessage; + !!this.props.mustUpgradeAppMessage || + props.showInvalidJoplinCloudCredential; } public registerCommands() { @@ -965,6 +980,7 @@ const mapStateToProps = (state: AppState) => { notesSortOrderField: state.settings['notes.sortOrder.field'], notesSortOrderReverse: state.settings['notes.sortOrder.reverse'], notesColumns: validateColumns(state.settings['notes.columns']), + showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate, }; }; diff --git a/packages/app-mobile/components/ScreenHeader/WarningBanner.test.tsx b/packages/app-mobile/components/ScreenHeader/WarningBanner.test.tsx index 11740452f..23cdb8d21 100644 --- a/packages/app-mobile/components/ScreenHeader/WarningBanner.test.tsx +++ b/packages/app-mobile/components/ScreenHeader/WarningBanner.test.tsx @@ -16,6 +16,7 @@ interface WrapperProps { mustUpgradeAppMessage?: string; shareInvitations?: ShareInvitation[]; processingShareInvitationResponse?: boolean; + showInvalidJoplinCloudCredential?: boolean; } const WarningBannerWrapper: React.FC = props => { @@ -29,6 +30,7 @@ const WarningBannerWrapper: React.FC = props => { mustUpgradeAppMessage={props.mustUpgradeAppMessage ?? ''} shareInvitations={props.shareInvitations ?? []} processingShareInvitationResponse={props.processingShareInvitationResponse ?? false} + showInvalidJoplinCloudCredential={props.showInvalidJoplinCloudCredential ?? false} />; }; diff --git a/packages/app-mobile/components/ScreenHeader/WarningBanner.tsx b/packages/app-mobile/components/ScreenHeader/WarningBanner.tsx index b4c225140..d8c8d5f52 100644 --- a/packages/app-mobile/components/ScreenHeader/WarningBanner.tsx +++ b/packages/app-mobile/components/ScreenHeader/WarningBanner.tsx @@ -19,6 +19,7 @@ interface Props { mustUpgradeAppMessage: string; shareInvitations: ShareInvitation[]; processingShareInvitationResponse: boolean; + showInvalidJoplinCloudCredential: boolean; } @@ -50,6 +51,9 @@ export const WarningBannerComponent: React.FC = props => { if (props.hasDisabledEncryptionItems) { warningComps.push(renderWarningBox('Status', _('Some items cannot be decrypted.'))); } + if (props.showInvalidJoplinCloudCredential) { + warningComps.push(renderWarningBox('JoplinCloudLogin', _('Your Joplin Cloud credentials are invalid, please login.'))); + } const shareInvitation = props.shareInvitations.find(inv => inv.status === ShareUserStatus.Waiting); if ( @@ -85,5 +89,6 @@ export default connect((state: AppState) => { mustUpgradeAppMessage: state.mustUpgradeAppMessage, shareInvitations: state.shareService.shareInvitations, processingShareInvitationResponse: state.shareService.processingShareInvitationResponse, + showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && state.mustAuthenticate, }; })(WarningBannerComponent); diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx index 3ac98c444..1405d8f8b 100644 --- a/packages/app-mobile/root.tsx +++ b/packages/app-mobile/root.tsx @@ -39,7 +39,7 @@ const { connect, Provider } = require('react-redux'); import { Provider as PaperProvider, MD3DarkTheme, MD3LightTheme } from 'react-native-paper'; const { BackButtonService } = require('./services/back-button.js'); import NavService from '@joplin/lib/services/NavService'; -import { createStore, applyMiddleware } from 'redux'; +import { createStore, applyMiddleware, Dispatch } from 'redux'; import reduxSharedMiddleware from '@joplin/lib/components/shared/reduxSharedMiddleware'; const { shimInit } = require('./utils/shim-init-react.js'); const { AppNav } = require('./components/app-nav.js'); @@ -493,7 +493,7 @@ const getInitialActiveFolder = async () => { }; // eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied -async function initialize(dispatch: Function) { +async function initialize(dispatch: Dispatch) { shimInit(); setDispatch(dispatch); @@ -535,6 +535,7 @@ async function initialize(dispatch: Function) { reg.setLogger(mainLogger); reg.setShowErrorMessageBoxHandler((message: string) => { alert(message); }); + reg.setDispatch(dispatch); BaseService.logger_ = mainLogger; // require('@joplin/lib/ntpDate').setLogger(reg.logger()); diff --git a/packages/lib/SyncTargetJoplinCloud.ts b/packages/lib/SyncTargetJoplinCloud.ts index bf3fb54c8..1b40ec7bd 100644 --- a/packages/lib/SyncTargetJoplinCloud.ts +++ b/packages/lib/SyncTargetJoplinCloud.ts @@ -56,7 +56,9 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget { const sessionId = await api.sessionId(); return !!sessionId; } catch (error) { - if (error.code === 403) return false; + if (error.code === 403) { + return false; + } throw error; } } @@ -65,8 +67,10 @@ export default class SyncTargetJoplinCloud extends BaseSyncTarget { return 'JoplinCloudLogin'; } + // While Joplin Cloud requires password, the new login method makes this + // information useless public static requiresPassword() { - return true; + return false; } public async fileApi(): Promise { diff --git a/packages/lib/Synchronizer.ts b/packages/lib/Synchronizer.ts index 9e0419ddb..56b98baa0 100644 --- a/packages/lib/Synchronizer.ts +++ b/packages/lib/Synchronizer.ts @@ -524,6 +524,9 @@ export default class Synchronizer { // await uploadSyncInfo(this.api(), remoteInfo); } } catch (error) { + if (error.code === 403) { + this.dispatch({ type: 'MUST_AUTHENTICATE', value: true }); + } if (error.code === 'outdatedSyncTarget') { Setting.setValue('sync.upgradeState', Setting.SYNC_UPGRADE_STATE_SHOULD_DO); } diff --git a/packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.ts b/packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.ts index ff8f1e4b7..4f2e8de38 100644 --- a/packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.ts +++ b/packages/lib/components/shared/config/shouldShowMissingPasswordWarning.test.ts @@ -8,7 +8,7 @@ const targetToRequiresPassword: Record = { 'webdav': true, 'amazon_s3': true, 'joplinServer': true, - 'joplinCloud': true, + 'joplinCloud': false, 'onedrive': false, 'dropbox': false, }; diff --git a/packages/lib/reducer.ts b/packages/lib/reducer.ts index ba230e4ec..0254aaa06 100644 --- a/packages/lib/reducer.ts +++ b/packages/lib/reducer.ts @@ -133,6 +133,7 @@ export interface State { lastDeletion: StateLastDeletion; lastDeletionNotificationTime: number; mustUpgradeAppMessage: string; + mustAuthenticate: boolean; // Extra reducer keys go here: pluginService: PluginServiceState; @@ -215,6 +216,7 @@ export const defaultState: State = { }, lastDeletionNotificationTime: 0, mustUpgradeAppMessage: '', + mustAuthenticate: false, pluginService: pluginServiceDefaultState, shareService: shareServiceDefaultState, @@ -1324,6 +1326,10 @@ const reducer = produce((draft: Draft = defaultState, action: any) => { draft.mustUpgradeAppMessage = action.message; break; + case 'MUST_AUTHENTICATE': + draft.mustAuthenticate = action.value; + break; + case 'NOTE_LIST_RENDERER_ADD': { const noteListRendererIds = draft.noteListRendererIds.slice(); diff --git a/packages/lib/registry.ts b/packages/lib/registry.ts index 69607812f..12b09ccf2 100644 --- a/packages/lib/registry.ts +++ b/packages/lib/registry.ts @@ -2,6 +2,7 @@ import Logger from '@joplin/utils/Logger'; import Setting from './models/Setting'; import shim from './shim'; import SyncTargetRegistry from './SyncTargetRegistry'; +import { AnyAction, Dispatch } from 'redux'; class Registry { @@ -21,6 +22,7 @@ class Registry { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied private db_: any; private isOnMobileData_ = false; + private dispatch_: Dispatch = (() => {}) as Dispatch; public logger() { if (!this.logger_) { @@ -45,6 +47,14 @@ class Registry { this.showErrorMessageBoxHandler_(message); } + public setDispatch(dispatch: Dispatch) { + this.dispatch_ = dispatch; + } + + private dispatch(action: AnyAction) { + return this.dispatch_(action); + } + // If isOnMobileData is true, the doWifiConnectionCheck is not set // and the sync.mobileWifiOnly setting is true it will cancel the sync. public setIsOnMobileData(isOnMobileData: boolean) { @@ -139,10 +149,18 @@ class Registry { } if (!(await this.syncTarget(syncTargetId).isAuthenticated())) { + this.dispatch({ + type: 'MUST_AUTHENTICATE', + value: true, + }); this.logger().info('Synchroniser is missing credentials - manual sync required to authenticate.'); promiseResolve(); return; } + this.dispatch({ + type: 'MUST_AUTHENTICATE', + value: false, + }); try { const sync = await this.syncTarget(syncTargetId).synchronizer();