import * as React from 'react'; import { useState } from 'react'; import { connect } from 'react-redux'; import { Platform } from 'react-native'; import { AppState } from '../../utils/types'; import WarningBox, { WarningBoxAction, WarningBoxTarget } from './WarningBox'; import NavService from '@joplin/lib/services/NavService'; import { _ } from '@joplin/lib/locale'; import { showMissingMasterKeyMessage, showUnknownKeyFormatBanner } from '@joplin/lib/services/e2ee/utils'; import { localSyncInfoFromState, noteLockKeyConflictFromState } from '@joplin/lib/services/synchronizer/syncInfoUtils'; import { NoteLockMigrationStatus } from '@joplin/lib/reducer'; import { confirmAndSkipNoteLockKeyMigration, notMigratedMessage, withSkippedCount } from '@joplin/lib/services/noteLock/NoteLockKeyMigration'; import { Dispatch } from 'redux'; import Setting from '@joplin/lib/models/Setting'; import { ShareInvitation, ShareUserStatus } from '@joplin/lib/services/share/reducer'; import { substrWithEllipsis } from '@joplin/lib/string-utils'; import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect'; import shim from '@joplin/lib/shim'; import Logger from '@joplin/utils/Logger'; import { reg } from '@joplin/lib/registry'; const logger = Logger.create('WarningBanner'); interface Props { themeId: number; showMissingMasterKeyMessage: boolean; showUnknownKeyFormatMessage: boolean; hasDisabledSyncItems: boolean; shouldUpgradeSyncTarget: boolean; showShouldUpgradeSyncTargetMessage: boolean|undefined; hasDisabledEncryptionItems: boolean; mustUpgradeAppMessage: string; syncTargetAppMinVersion?: string; shareInvitations: ShareInvitation[]; processingShareInvitationResponse: boolean; showInvalidJoplinCloudCredential: boolean; noteLockKeyConflict: boolean; showNoteLockKeyConflictMessage: boolean|undefined; noteLockMigrationStatus: NoteLockMigrationStatus|null; dispatch: Dispatch; } const androidGooglePlayUrl = 'https://play.google.com/store/apps/details?id=net.cozic.joplin'; const androidPreReleaseUrl = 'https://github.com/laurent22/joplin-android/tags'; const iosAppStoreUrl = 'https://apps.apple.com/app/id1315599797'; const fetchAndroidVersionIsPreRelease = async (version: string) => { const response = await shim.fetch(`https://api.github.com/repos/laurent22/joplin-android/releases/tags/android-v${version}`); if (!response.ok) return null; const release = await response.json(); return !!release.prerelease; }; const WarningBannerComponent: React.FC = props => { const warningComps = []; const [isAndroidTargetPreRelease, setIsAndroidTargetPreRelease] = useState(null); useAsyncEffect(async event => { setIsAndroidTargetPreRelease(null); if (Platform.OS !== 'android' || !props.mustUpgradeAppMessage || !props.syncTargetAppMinVersion) return; try { const isPreRelease = await fetchAndroidVersionIsPreRelease(props.syncTargetAppMinVersion); if (!event.cancelled) setIsAndroidTargetPreRelease(isPreRelease); } catch (error) { logger.error('Could not load release metadata for version', props.syncTargetAppMinVersion, error); } }, [props.mustUpgradeAppMessage, props.syncTargetAppMinVersion]); const renderWarningBox = (key: string, message: string, target: WarningBoxTarget, actions?: WarningBoxAction[]) => { return ; }; const renderMustUpgradeAppMessage = () => { if (props.syncTargetAppMinVersion) { const upgradeMessage = (message: string) => _( 'In order to synchronise, Please upgrade your application to version %s: %s', props.syncTargetAppMinVersion, message, ); if (Platform.OS === 'android' && isAndroidTargetPreRelease !== null) { if (isAndroidTargetPreRelease) { return renderWarningBox( 'UpgradeApp', upgradeMessage(_('Download it from the Joplin Android repository')), { url: androidPreReleaseUrl }, ); } return renderWarningBox( 'UpgradeApp', upgradeMessage(_('Update it from Google Play')), { url: androidGooglePlayUrl }, ); } if (Platform.OS === 'ios') { return renderWarningBox( 'UpgradeApp', upgradeMessage(_('Update it from the App Store')), { url: iosAppStoreUrl }, ); } return renderWarningBox('UpgradeApp', _('In order to synchronise, Please upgrade your application to version %s', props.syncTargetAppMinVersion), null); } return renderWarningBox('UpgradeApp', props.mustUpgradeAppMessage, null); }; if (props.showMissingMasterKeyMessage) { warningComps.push(renderWarningBox('missingDecryptionPassword', _('Press to set the decryption password.'), { screen: 'EncryptionConfig' })); } if (props.showUnknownKeyFormatMessage) { warningComps.push( renderWarningBox('unknownKeyFormat', _('One or more encryption keys are stored in an unknown format. Press for more info.'), { screen: 'EncryptionConfig' }), ); } if (props.hasDisabledSyncItems) { warningComps.push(renderWarningBox('cannotSync', _('Some items cannot be synchronised. Press for more info.'), { screen: 'Status' })); } if (props.shouldUpgradeSyncTarget && props.showShouldUpgradeSyncTargetMessage !== false) { warningComps.push(renderWarningBox('upgradeSync', _('The sync target needs to be upgraded. Press this banner to proceed.'), { screen: 'UpgradeSyncTarget' })); } if (props.mustUpgradeAppMessage) { warningComps.push(renderMustUpgradeAppMessage()); } if (props.hasDisabledEncryptionItems) { warningComps.push(renderWarningBox('cannotDecrypt', _('Some items cannot be decrypted.'), { screen: 'Status' })); } const migration = props.noteLockMigrationStatus; if (props.showNoteLockKeyConflictMessage !== false) { if (migration?.running) { warningComps.push(renderWarningBox('noteLockMigration', _('Re-encrypting your locked notes with the synced key. Migration is in progress...'), null)); } else if (migration?.failed || migration?.awaitingDecryption) { warningComps.push(renderWarningBox('noteLockMigration', notMigratedMessage(migration), null, [ { title: _('Retry'), onPress: () => void NavService.go('NoteLockMigration') }, { title: _('Skip'), onPress: () => void confirmAndSkipNoteLockKeyMigration(props.dispatch) }, ])); } else if (migration) { const onDismiss = () => props.dispatch({ type: 'NOTE_LOCK_MIGRATION_STATUS_SET', value: null }); warningComps.push(renderWarningBox('noteLockMigration', `${withSkippedCount(_('The migration of your locked notes has completed.'), migration)} ${_('Press to dismiss.')}`, { onPress: onDismiss })); } else if (props.noteLockKeyConflict) { warningComps.push(renderWarningBox('noteLockMigration', _('The sync target uses a different note lock key to the one on your device. Press to migrate your locked notes.'), { screen: 'NoteLockMigration' })); } } if (props.showInvalidJoplinCloudCredential) { const target = { screen: 'JoplinCloudLogin' }; warningComps.push(renderWarningBox('syncLogin', _('Your Joplin Cloud credentials are invalid, please login.'), target)); } const shareInvitation = props.shareInvitations.find(inv => inv.status === ShareUserStatus.Waiting); if ( !props.processingShareInvitationResponse && !!shareInvitation ) { const invitation = props.shareInvitations.find(inv => inv.status === ShareUserStatus.Waiting); const sharer = invitation.share.user; warningComps.push(renderWarningBox( 'incomingShare', _('%s (%s) would like to share a notebook with you.', substrWithEllipsis(sharer?.full_name ?? 'Unknown', 0, 48), substrWithEllipsis(sharer?.email ?? 'Unknown', 0, 52)), { screen: 'ShareManager' }, )); } return warningComps; }; const isSyncLoginRoute = (state: AppState) => { const syncTargetId = state.settings['sync.target']; const syncTarget = syncTargetId ? reg.syncTarget(syncTargetId) : null; if (syncTarget) { return state.route?.routeName === syncTarget.authRouteName(); } return false; }; const isEncryptionConfigRoute = (state: AppState) => { return state.route?.routeName === 'EncryptionConfig'; }; export default connect((state: AppState) => { const syncInfo = localSyncInfoFromState(state); return { themeId: state.settings.theme, hasDisabledEncryptionItems: state.hasDisabledEncryptionItems, noteSelectionEnabled: state.noteSelectionEnabled, selectedFolderId: state.selectedFolderId, notesParentType: state.notesParentType, showMissingMasterKeyMessage: !isEncryptionConfigRoute(state) && showMissingMasterKeyMessage(syncInfo, state.notLoadedMasterKeys), showUnknownKeyFormatMessage: !isEncryptionConfigRoute(state) && showUnknownKeyFormatBanner(syncInfo), hasDisabledSyncItems: state.hasDisabledSyncItems, shouldUpgradeSyncTarget: state.settings['sync.upgradeState'] === Setting.SYNC_UPGRADE_STATE_SHOULD_DO, mustUpgradeAppMessage: state.mustUpgradeAppMessage, syncTargetAppMinVersion: state.mustUpgradeAppMinVersion, shareInvitations: state.shareService.shareInvitations, processingShareInvitationResponse: state.shareService.processingShareInvitationResponse, showInvalidJoplinCloudCredential: state.settings['sync.target'] === 10 && !isSyncLoginRoute(state) && state.mustAuthenticate, noteLockKeyConflict: noteLockKeyConflictFromState(state), noteLockMigrationStatus: state.noteLockMigrationStatus, }; })(WarningBannerComponent);