From 0cb65eac75db67e86cf94d5f03fcb16c893bf7f0 Mon Sep 17 00:00:00 2001 From: Matej Lubej Date: Tue, 21 May 2024 13:28:28 +0200 Subject: [PATCH 1/6] Add capawesome/capacitor-app-update --- android/app/capacitor.build.gradle | 1 + android/capacitor.settings.gradle | 3 +++ ios/App/Podfile | 1 + package.json | 1 + yarn.lock | 5 +++++ 5 files changed, 11 insertions(+) diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 6771f13b4d..5abdb54f69 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -11,6 +11,7 @@ apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { implementation project(':capacitor-community-bluetooth-le') implementation project(':capacitor-app') + implementation project(':capawesome-capacitor-app-update') } diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 61d0d1acc5..5ff434f8c9 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -7,3 +7,6 @@ project(':capacitor-community-bluetooth-le').projectDir = new File('../node_modu include ':capacitor-app' project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') + +include ':capawesome-capacitor-app-update' +project(':capawesome-capacitor-app-update').projectDir = new File('../node_modules/@capawesome/capacitor-app-update/android') diff --git a/ios/App/Podfile b/ios/App/Podfile index 7515e4d97f..e542648197 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -13,6 +13,7 @@ def capacitor_pods pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCommunityBluetoothLe', :path => '../../node_modules/@capacitor-community/bluetooth-le' pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' + pod 'CapawesomeCapacitorAppUpdate', :path => '../../node_modules/@capawesome/capacitor-app-update' end target 'App' do diff --git a/package.json b/package.json index 93723bafee..63095d4113 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@capacitor/app": "6.0.0", "@capacitor/core": "6.0.0", "@capacitor/ios": "6.0.0", + "@capawesome/capacitor-app-update": "6.0.0", "@ethereumjs/util": "9.0.3", "@ledgerhq/hw-transport-webusb": "6.28.5", "@metamask/browser-passworder": "=3.0.0", diff --git a/yarn.lock b/yarn.lock index 12ff4eacc2..0aa1f1de42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1192,6 +1192,11 @@ resolved "https://registry.yarnpkg.com/@capacitor/ios/-/ios-6.0.0.tgz#b04ce2967d732c73ebf29ec38b4b585255930e43" integrity sha512-7mAs3gjWiE5DPM4AGPduqFSDGXCPwwqQRMzbohVway7/cTWnHomHV8mIojMZE4GILeWO2fILbyu3C8q9pHg2vg== +"@capawesome/capacitor-app-update@6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@capawesome/capacitor-app-update/-/capacitor-app-update-6.0.0.tgz#cda175030417f5bfa3b2b7c8bbe22a531854aa40" + integrity sha512-T4k0dC3XoJDgtX1QOQx0Rr5eMi3NhJn93QawQ6ZYYMmssnqCoRPpo2rg3VKVKMUZgAmIUN5Rwons9T4QkjTOzA== + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" From 673e760bd41bcbcdfd58e66609d63755374d5129 Mon Sep 17 00:00:00 2001 From: Matej Lubej Date: Tue, 21 May 2024 16:30:17 +0200 Subject: [PATCH 2/6] Add Android update wall --- .../__snapshots__/index.test.tsx.snap | 4 +-- src/app/components/Ionic/IonicProvider.tsx | 21 ----------- .../IonicNativePlatformProvider/index.tsx | 16 +++++++++ .../Ionic/components/UpdateGate/index.tsx | 36 +++++++++++++++++++ ....tsx => useIonicAppStateChangeListener.ts} | 0 .../Ionic/hooks/useIonicRequiresUpdate.ts | 23 ++++++++++++ .../Ionic/providers/IonicContext.ts | 17 +++++++++ .../Ionic/providers/IonicProvider.tsx | 23 ++++++++++++ .../Ionic/utils/capacitor-app-update.ts | 32 +++++++++++++++++ src/app/index.tsx | 6 ++-- 10 files changed, 152 insertions(+), 26 deletions(-) delete mode 100644 src/app/components/Ionic/IonicProvider.tsx create mode 100644 src/app/components/Ionic/components/IonicNativePlatformProvider/index.tsx create mode 100644 src/app/components/Ionic/components/UpdateGate/index.tsx rename src/app/components/Ionic/hooks/{useIonicAppStateChangeListener.tsx => useIonicAppStateChangeListener.ts} (100%) create mode 100644 src/app/components/Ionic/hooks/useIonicRequiresUpdate.ts create mode 100644 src/app/components/Ionic/providers/IonicContext.ts create mode 100644 src/app/components/Ionic/providers/IonicProvider.tsx create mode 100644 src/app/components/Ionic/utils/capacitor-app-update.ts diff --git a/src/app/__tests__/__snapshots__/index.test.tsx.snap b/src/app/__tests__/__snapshots__/index.test.tsx.snap index 08bbc9f1fa..58d0c26986 100644 --- a/src/app/__tests__/__snapshots__/index.test.tsx.snap +++ b/src/app/__tests__/__snapshots__/index.test.tsx.snap @@ -2,7 +2,7 @@ exports[` should render and match the snapshot 1`] = ` - + should render and match the snapshot 1`] = ` - + `; diff --git a/src/app/components/Ionic/IonicProvider.tsx b/src/app/components/Ionic/IonicProvider.tsx deleted file mode 100644 index 7e7b538230..0000000000 --- a/src/app/components/Ionic/IonicProvider.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { createContext, FC, PropsWithChildren } from 'react' -import { Capacitor } from '@capacitor/core' -import { useIonicBackButtonListener } from './hooks/useIonicBackButtonListener' -import { useIonicAppStateChangeListener } from './hooks/useIonicAppStateChangeListener' - -const IonicContext = createContext(undefined) - -const IonicContextProvider: FC = ({ children }) => { - useIonicBackButtonListener() - useIonicAppStateChangeListener() - - return {children} -} - -export const IonicProvider: FC = ({ children }) => { - if (Capacitor.isNativePlatform()) { - return {children} - } - - return children -} diff --git a/src/app/components/Ionic/components/IonicNativePlatformProvider/index.tsx b/src/app/components/Ionic/components/IonicNativePlatformProvider/index.tsx new file mode 100644 index 0000000000..e16f2539d8 --- /dev/null +++ b/src/app/components/Ionic/components/IonicNativePlatformProvider/index.tsx @@ -0,0 +1,16 @@ +import { FC, PropsWithChildren } from 'react' +import { Capacitor } from '@capacitor/core' +import { IonicContextProvider } from '../../providers/IonicProvider' +import { UpdateGate } from '../UpdateGate' + +export const IonicNativePlatformProvider: FC = ({ children }) => { + if (Capacitor.isNativePlatform()) { + return ( + + {children} + + ) + } + + return children +} diff --git a/src/app/components/Ionic/components/UpdateGate/index.tsx b/src/app/components/Ionic/components/UpdateGate/index.tsx new file mode 100644 index 0000000000..613f2d197f --- /dev/null +++ b/src/app/components/Ionic/components/UpdateGate/index.tsx @@ -0,0 +1,36 @@ +import React, { FC, PropsWithChildren, useContext } from 'react' +import { IonicContext } from '../../providers/IonicContext' +import { Box } from 'grommet/es6/components/Box' +import { Button } from 'grommet/es6/components/Button' +import { navigateToAppStore } from '../../utils/capacitor-app-update' +import { Paragraph } from 'grommet/es6/components/Paragraph' + +export const UpdateGate: FC = ({ children }) => { + const { + state: { requiresUpdate }, + } = useContext(IonicContext) + + if (requiresUpdate === false) return children + + const handleNavigateToAppStore = () => { + navigateToAppStore() + } + + return ( + + {requiresUpdate === undefined && ( + + Loading... + + )} + {requiresUpdate === true && ( + + + This app requires an update to the latest version in order to operate correctly. + + + )} )} diff --git a/src/app/components/Ionic/hooks/useIonicRequiresUpdate.ts b/src/app/components/Ionic/hooks/useIonicRequiresUpdate.ts index 14d3902f27..d9df422e59 100644 --- a/src/app/components/Ionic/hooks/useIonicRequiresUpdate.ts +++ b/src/app/components/Ionic/hooks/useIonicRequiresUpdate.ts @@ -1,23 +1,35 @@ -import { useEffect } from 'react' -import { IonicProviderState } from '../providers/IonicContext' +import { Dispatch, SetStateAction, useEffect } from 'react' +import { IonicProviderState, UpdateAvailability } from '../providers/IonicContext' import { updateAvailable } from '../utils/capacitor-app-update' -export const useIonicRequiresUpdate = (setState: (state: IonicProviderState) => void) => { - useEffect(() => { - let inProgress = true +export const useIonicRequiresUpdate = ( + state: IonicProviderState, + setState: Dispatch>, +) => { + const checkForUpdateAvailability = async () => { + if (state.updateAvailability === UpdateAvailability.LOADING) { + return + } + + setState(prevState => ({ ...prevState, updateAvailability: UpdateAvailability.LOADING })) - const init = async () => { - const requiresUpdate = await updateAvailable() + try { + const updateAvailability = await updateAvailable() - if (inProgress) { - setState({ requiresUpdate }) - } + setState(prevState => ({ ...prevState, updateAvailability })) + } catch (error) { + setState(prevState => ({ + ...prevState, + updateAvailability: UpdateAvailability.ERROR, + error: error as Error, + })) } + } - init() + useEffect(() => { + checkForUpdateAvailability() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) - return () => { - inProgress = false - } - }, [setState]) + return { checkForUpdateAvailability } } diff --git a/src/app/components/Ionic/providers/IonicContext.ts b/src/app/components/Ionic/providers/IonicContext.ts index 9ef73c172b..133bcbf49b 100644 --- a/src/app/components/Ionic/providers/IonicContext.ts +++ b/src/app/components/Ionic/providers/IonicContext.ts @@ -1,17 +1,23 @@ import { createContext } from 'react' +export enum UpdateAvailability { + NOT_INITIALIZED, + LOADING, + UPDATE_AVAILABLE, + UPDATE_NOT_AVAILABLE, + UPDATE_IN_PROGRESS, + ERROR, + UNKNOWN, +} + export interface IonicProviderState { - /** - * Indicates whether an update is required. - * In an undefined state, it indicates that the application is currently in loading phase. - * - * @type {boolean | undefined} - */ - requiresUpdate: boolean | undefined + updateAvailability: UpdateAvailability + error: Error | null } export interface IonicProviderContext { readonly state: IonicProviderState + checkForUpdateAvailability: () => void } export const IonicContext = createContext({} as IonicProviderContext) diff --git a/src/app/components/Ionic/providers/IonicProvider.tsx b/src/app/components/Ionic/providers/IonicProvider.tsx index c32c761491..8ee73def2f 100644 --- a/src/app/components/Ionic/providers/IonicProvider.tsx +++ b/src/app/components/Ionic/providers/IonicProvider.tsx @@ -1,22 +1,24 @@ import { FC, PropsWithChildren, useState } from 'react' import { useIonicBackButtonListener } from '../hooks/useIonicBackButtonListener' import { useIonicAppStateChangeListener } from '../hooks/useIonicAppStateChangeListener' -import { IonicContext, IonicProviderContext, IonicProviderState } from './IonicContext' +import { IonicContext, IonicProviderContext, IonicProviderState, UpdateAvailability } from './IonicContext' import { useIonicRequiresUpdate } from '../hooks/useIonicRequiresUpdate' const ionicProviderInitialState: IonicProviderState = { - requiresUpdate: undefined, + updateAvailability: UpdateAvailability.NOT_INITIALIZED, + error: null, } export const IonicContextProvider: FC = ({ children }) => { const [state, setState] = useState({ ...ionicProviderInitialState }) - useIonicRequiresUpdate(setState) + const { checkForUpdateAvailability } = useIonicRequiresUpdate(state, setState) useIonicAppStateChangeListener() useIonicBackButtonListener() const providerState: IonicProviderContext = { state, + checkForUpdateAvailability, } return {children} diff --git a/src/app/components/Ionic/utils/capacitor-app-update.ts b/src/app/components/Ionic/utils/capacitor-app-update.ts index b50cfb5e30..4b9ea68a3c 100644 --- a/src/app/components/Ionic/utils/capacitor-app-update.ts +++ b/src/app/components/Ionic/utils/capacitor-app-update.ts @@ -1,26 +1,32 @@ -import { AppUpdate, AppUpdateAvailability } from '@capawesome/capacitor-app-update' +import { + AppUpdate, + AppUpdateAvailability as IonicAppUpdateAvailability, +} from '@capawesome/capacitor-app-update' import { Capacitor } from '@capacitor/core' +import { UpdateAvailability } from '../providers/IonicContext' -export const updateAvailable = async () => { +// TODO: Skip on local builds +export const updateAvailable = async (): Promise => { const result = await AppUpdate.getAppUpdateInfo() const { updateAvailability, currentVersionCode, availableVersionCode } = result switch (updateAvailability) { - case AppUpdateAvailability.UPDATE_IN_PROGRESS: - return undefined - case AppUpdateAvailability.UPDATE_NOT_AVAILABLE: - return false + case IonicAppUpdateAvailability.UPDATE_IN_PROGRESS: + return UpdateAvailability.UPDATE_IN_PROGRESS + case IonicAppUpdateAvailability.UPDATE_NOT_AVAILABLE: + return UpdateAvailability.UPDATE_NOT_AVAILABLE // Returns UNKNOWN when unable to determine with mobile app store if update is available or not - case AppUpdateAvailability.UNKNOWN: - return true + case IonicAppUpdateAvailability.UNKNOWN: + return UpdateAvailability.UNKNOWN } - if (Capacitor.getPlatform() === 'android') { - // Example of version code -> "1", "2", ... - return ( - parseInt(availableVersionCode ?? `${Number.MAX_SAFE_INTEGER}`, 10) > + // Example of version code -> "1", "2", ... + if ( + Capacitor.getPlatform() === 'android' && + parseInt(availableVersionCode ?? `${Number.MAX_SAFE_INTEGER}`, 10) > parseInt(currentVersionCode ?? '0', 10) - ) + ) { + return UpdateAvailability.UPDATE_AVAILABLE } // TODO: Add for iOS diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 22733a8e88..8ef6dfa797 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -212,14 +212,14 @@ "header": "Access existing wallet" } }, + "infoBox": { + "valueCopied": "{{label}} copied." + }, "icons": { "wallet": { "label": "Wallet" } }, - "infoBox": { - "valueCopied": "{{label}} copied." - }, "language": "Language", "ledger": { "extension": { @@ -278,9 +278,15 @@ "title": "Important Wallet Update" }, "mobileUpdate": { - "updateDescription": "A new update is available for your ROSE Wallet. We recommend updating to the latest version for bug fixes, enhanced security and new features.", - "updateNow": "Update now", - "updatePending": "Update pending..." + "errorTitle": "Unexpected error", + "retry": "Retry", + "unknownOrErrorDescription": "Apologies for the inconvenience, an unexpected error has transpired. Please verify your internet connection and retry by clicking on the \"Retry\" button.", + "unknownTitle": "Unknown error", + "updateAvailableDescription": "A new update is available for your ROSE Wallet. We recommend updating to the latest version for bug fixes, enhanced security and new features.", + "updateAvailableTitle": "Update pending...", + "updateInProgressDescription": "Your ROSE Wallet is currently undergoing an update. Please check back at later time. Alternatively, you may choose to retry by clicking on the \"Retry\" button.", + "updateInProgressTitle": "Update in progress...", + "updateNow": "Update now" }, "openWallet": { "bitpie": { From 202c9d02dfc346596a12c68012d0aabf2b5092c2 Mon Sep 17 00:00:00 2001 From: Matej Lubej Date: Tue, 4 Jun 2024 16:19:41 +0200 Subject: [PATCH 6/6] Add Skip button in case of an error on update screen --- .../Ionic/components/UpdateGate/index.tsx | 31 +++++++++++++++---- .../Ionic/hooks/useIonicRequiresUpdate.ts | 6 +++- .../Ionic/providers/IonicContext.ts | 1 + .../Ionic/providers/IonicProvider.tsx | 3 +- src/locales/en/translation.json | 5 +-- 5 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/app/components/Ionic/components/UpdateGate/index.tsx b/src/app/components/Ionic/components/UpdateGate/index.tsx index 5c1540a3b6..e0ba35634c 100644 --- a/src/app/components/Ionic/components/UpdateGate/index.tsx +++ b/src/app/components/Ionic/components/UpdateGate/index.tsx @@ -38,7 +38,9 @@ const CTAButton = styled(Button)` border-radius: 8px; ` -const getUpdateStatusMap: (t: TFunction) => { [key in UpdateAvailability]?: any } = t => ({ +const getUpdateStatusMap: (t: TFunction) => { + [key in UpdateAvailability]?: { title: string; desc: string } +} = t => ({ [UpdateAvailability.UPDATE_AVAILABLE]: { title: t('mobileUpdate.updateAvailableTitle', 'Update pending...'), desc: t( @@ -50,21 +52,24 @@ const getUpdateStatusMap: (t: TFunction) => { [key in UpdateAvailability]?: any title: t('mobileUpdate.updateInProgressTitle', 'Update in progress...'), desc: t( 'mobileUpdate.updateInProgressDescription', - 'Your ROSE Wallet is currently undergoing an update. Please check back at later time. Alternatively, you may choose to retry by clicking on the "Retry" button.', + 'Your ROSE Wallet is currently undergoing an update. Please check back at later time. Alternatively, you may choose to retry by clicking on the "{{retryButtonLabel}}" button.', + { retryButtonLabel: t('mobileUpdate.retry', 'Retry') }, ), }, [UpdateAvailability.UNKNOWN]: { title: t('mobileUpdate.unknownTitle', 'Unknown error'), desc: t( 'mobileUpdate.unknownOrErrorDescription', - 'Apologies for the inconvenience, an unexpected error has transpired. Please verify your internet connection and retry by clicking on the "Retry" button.', + 'Apologies for the inconvenience, an unexpected error has transpired. Please verify your internet connection and retry by clicking on the "{{retryButtonLabel}}" button.', + { retryButtonLabel: t('mobileUpdate.retry', 'Retry') }, ), }, [UpdateAvailability.ERROR]: { title: t('mobileUpdate.errorTitle', 'Unexpected error'), desc: t( 'mobileUpdate.unknownOrErrorDescription', - 'Apologies for the inconvenience, an unexpected error has transpired. Please verify your internet connection and retry by clicking on the "Retry" button.', + 'Apologies for the inconvenience, an unexpected error has transpired. Please verify your internet connection and retry by clicking on the "{{retryButtonLabel}}" button.', + { retryButtonLabel: t('mobileUpdate.retry', 'Retry') }, ), }, }) @@ -75,6 +80,7 @@ export const UpdateGate: FC = ({ children }) => { const { state: { updateAvailability }, checkForUpdateAvailability, + skipUpdate, } = useContext(IonicContext) if (updateAvailability === UpdateAvailability.UPDATE_NOT_AVAILABLE) return children @@ -143,9 +149,22 @@ export const UpdateGate: FC = ({ children }) => { /> )} {updateAvailability !== UpdateAvailability.UPDATE_AVAILABLE && ( - )} diff --git a/src/app/components/Ionic/hooks/useIonicRequiresUpdate.ts b/src/app/components/Ionic/hooks/useIonicRequiresUpdate.ts index d9df422e59..de683b2389 100644 --- a/src/app/components/Ionic/hooks/useIonicRequiresUpdate.ts +++ b/src/app/components/Ionic/hooks/useIonicRequiresUpdate.ts @@ -31,5 +31,9 @@ export const useIonicRequiresUpdate = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, []) - return { checkForUpdateAvailability } + const skipUpdate = () => { + setState(prevState => ({ ...prevState, updateAvailability: UpdateAvailability.UPDATE_NOT_AVAILABLE })) + } + + return { checkForUpdateAvailability, skipUpdate } } diff --git a/src/app/components/Ionic/providers/IonicContext.ts b/src/app/components/Ionic/providers/IonicContext.ts index 133bcbf49b..9a76712628 100644 --- a/src/app/components/Ionic/providers/IonicContext.ts +++ b/src/app/components/Ionic/providers/IonicContext.ts @@ -18,6 +18,7 @@ export interface IonicProviderState { export interface IonicProviderContext { readonly state: IonicProviderState checkForUpdateAvailability: () => void + skipUpdate: () => void } export const IonicContext = createContext({} as IonicProviderContext) diff --git a/src/app/components/Ionic/providers/IonicProvider.tsx b/src/app/components/Ionic/providers/IonicProvider.tsx index 8ee73def2f..e9461f04b8 100644 --- a/src/app/components/Ionic/providers/IonicProvider.tsx +++ b/src/app/components/Ionic/providers/IonicProvider.tsx @@ -12,13 +12,14 @@ const ionicProviderInitialState: IonicProviderState = { export const IonicContextProvider: FC = ({ children }) => { const [state, setState] = useState({ ...ionicProviderInitialState }) - const { checkForUpdateAvailability } = useIonicRequiresUpdate(state, setState) + const { checkForUpdateAvailability, skipUpdate } = useIonicRequiresUpdate(state, setState) useIonicAppStateChangeListener() useIonicBackButtonListener() const providerState: IonicProviderContext = { state, checkForUpdateAvailability, + skipUpdate, } return {children} diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 8ef6dfa797..794c22191b 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -279,12 +279,13 @@ }, "mobileUpdate": { "errorTitle": "Unexpected error", + "later": "Later", "retry": "Retry", - "unknownOrErrorDescription": "Apologies for the inconvenience, an unexpected error has transpired. Please verify your internet connection and retry by clicking on the \"Retry\" button.", + "unknownOrErrorDescription": "Apologies for the inconvenience, an unexpected error has transpired. Please verify your internet connection and retry by clicking on the \"{{retryButtonLabel}}\" button.", "unknownTitle": "Unknown error", "updateAvailableDescription": "A new update is available for your ROSE Wallet. We recommend updating to the latest version for bug fixes, enhanced security and new features.", "updateAvailableTitle": "Update pending...", - "updateInProgressDescription": "Your ROSE Wallet is currently undergoing an update. Please check back at later time. Alternatively, you may choose to retry by clicking on the \"Retry\" button.", + "updateInProgressDescription": "Your ROSE Wallet is currently undergoing an update. Please check back at later time. Alternatively, you may choose to retry by clicking on the \"{{retryButtonLabel}}\" button.", "updateInProgressTitle": "Update in progress...", "updateNow": "Update now" },