diff --git a/apps/native/app/ios/IslandApp.xcodeproj/project.pbxproj b/apps/native/app/ios/IslandApp.xcodeproj/project.pbxproj
index b13fc775ca43..a6c26ffd0c01 100644
--- a/apps/native/app/ios/IslandApp.xcodeproj/project.pbxproj
+++ b/apps/native/app/ios/IslandApp.xcodeproj/project.pbxproj
@@ -550,7 +550,10 @@
"-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1",
);
- OTHER_LDFLAGS = "$(inherited) ";
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ " ",
+ );
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
@@ -622,7 +625,10 @@
"-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1",
);
- OTHER_LDFLAGS = "$(inherited) ";
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ " ",
+ );
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
@@ -742,7 +748,10 @@
"-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1",
);
- OTHER_LDFLAGS = "$(inherited) ";
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ " ",
+ );
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
USE_HERMES = true;
diff --git a/apps/native/app/ios/Podfile.lock b/apps/native/app/ios/Podfile.lock
index d751cd655a22..33c82a8973d9 100644
--- a/apps/native/app/ios/Podfile.lock
+++ b/apps/native/app/ios/Podfile.lock
@@ -33,7 +33,7 @@ PODS:
- ExpoModulesCore
- ExpoFileSystem (17.0.1):
- ExpoModulesCore
- - ExpoFont (12.0.9):
+ - ExpoFont (12.0.10):
- ExpoModulesCore
- ExpoHaptics (13.0.1):
- ExpoModulesCore
@@ -1923,7 +1923,7 @@ SPEC CHECKSUMS:
Expo: 88047e6d12a8113a18887b6ebd775fccfcdbf3c9
ExpoAsset: 323700f291684f110fb55f0d4022a3362ea9f875
ExpoFileSystem: 80bfe850b1f9922c16905822ecbf97acd711dc51
- ExpoFont: e7f2275c10ca8573c991e007329ad6bf98086485
+ ExpoFont: 00756e6c796d8f7ee8d211e29c8b619e75cbf238
ExpoHaptics: 5a3a88971af384255baf2504f38b41189cec6984
ExpoKeepAwake: 3b8815d9dd1d419ee474df004021c69fdd316d08
ExpoLocalAuthentication: 9e02a56a4cf9868f0052656a93d4c94101a42ed7
diff --git a/apps/native/app/package.json b/apps/native/app/package.json
index fdcf1cede4ab..c7319cdd1cdb 100644
--- a/apps/native/app/package.json
+++ b/apps/native/app/package.json
@@ -52,6 +52,7 @@
"@react-native/metro-config": "0.74.87",
"@react-native/typescript-config": "0.74.87",
"apollo3-cache-persist": "0.15.0",
+ "compare-versions": "6.1.1",
"configcat-js": "7.0.0",
"dynamic-color": "0.3.0",
"expo": "51.0.25",
diff --git a/apps/native/app/src/assets/illustrations/digital-services-m1-dots.png b/apps/native/app/src/assets/illustrations/digital-services-m1-dots.png
new file mode 100644
index 000000000000..12acfb1ff9f5
Binary files /dev/null and b/apps/native/app/src/assets/illustrations/digital-services-m1-dots.png differ
diff --git a/apps/native/app/src/assets/illustrations/digital-services-m1-dots@2x.png b/apps/native/app/src/assets/illustrations/digital-services-m1-dots@2x.png
new file mode 100644
index 000000000000..f663e415ec12
Binary files /dev/null and b/apps/native/app/src/assets/illustrations/digital-services-m1-dots@2x.png differ
diff --git a/apps/native/app/src/assets/illustrations/digital-services-m1-dots@3x.png b/apps/native/app/src/assets/illustrations/digital-services-m1-dots@3x.png
new file mode 100644
index 000000000000..23a3b9b71ec9
Binary files /dev/null and b/apps/native/app/src/assets/illustrations/digital-services-m1-dots@3x.png differ
diff --git a/apps/native/app/src/graphql/client.ts b/apps/native/app/src/graphql/client.ts
index fbac7b3719a1..2e4e29eb5d34 100644
--- a/apps/native/app/src/graphql/client.ts
+++ b/apps/native/app/src/graphql/client.ts
@@ -20,6 +20,7 @@ import { environmentStore } from '../stores/environment-store'
import { createMMKVStorage } from '../stores/mmkv'
import { offlineStore } from '../stores/offline-store'
import { MainBottomTabs } from '../utils/component-registry'
+import { getCustomUserAgent } from '../utils/user-agent'
const apolloMMKVStorage = createMMKVStorage({ withEncryption: true })
@@ -134,6 +135,7 @@ const authLink = setContext(async (_, { headers }) => ({
'X-Cognito-Token': `Bearer ${
environmentStore.getState().cognito?.accessToken
}`,
+ 'User-Agent': getCustomUserAgent(),
cookie: [authStore.getState().cookies]
.filter((x) => String(x) !== '')
.join('; '),
diff --git a/apps/native/app/src/messages/en.ts b/apps/native/app/src/messages/en.ts
index 0662636e4777..845661117191 100644
--- a/apps/native/app/src/messages/en.ts
+++ b/apps/native/app/src/messages/en.ts
@@ -596,4 +596,11 @@ export const en: TranslatedMessages = {
'passkeys.skipButton': 'Skip',
'passkeys.errorRegistering': 'Error',
'passkeys.errorRegisteringMessage': 'Could not create a passkey',
+
+ // update app
+ 'updateApp.title': 'Update app',
+ 'updateApp.description':
+ 'You are about to use an old version of the Island.is app. Please update the app to be able to continue.',
+ 'updateApp.button': 'Update',
+ 'updateApp.buttonSkip': 'Skip',
}
diff --git a/apps/native/app/src/messages/is.ts b/apps/native/app/src/messages/is.ts
index 19e80cf23b38..303d26dfa3e0 100644
--- a/apps/native/app/src/messages/is.ts
+++ b/apps/native/app/src/messages/is.ts
@@ -595,4 +595,11 @@ export const is = {
'passkeys.skipButton': 'Sleppa',
'passkeys.errorRegistering': 'Villa',
'passkeys.errorRegisteringMessage': 'Tókst ekki að búa til aðgangslykil',
+
+ // update app
+ 'updateApp.title': 'Uppfæra app',
+ 'updateApp.description':
+ 'Þú ert að fara að nota gamla útgáfu af Ísland.is appinu. Vinsamlegast uppfærðu appið til að halda áfram.',
+ 'updateApp.button': 'Uppfæra',
+ 'updateApp.buttonSkip': 'Sleppa',
}
diff --git a/apps/native/app/src/screens/home/home.tsx b/apps/native/app/src/screens/home/home.tsx
index 07106e266c41..057b658de585 100644
--- a/apps/native/app/src/screens/home/home.tsx
+++ b/apps/native/app/src/screens/home/home.tsx
@@ -28,8 +28,10 @@ import {
} from '../../stores/preferences-store'
import { useUiStore } from '../../stores/ui-store'
import { isAndroid } from '../../utils/devices'
+import { needsToUpdateAppVersion } from '../../utils/minimum-app-version'
import { getRightButtons } from '../../utils/get-main-root'
import { testIDs } from '../../utils/test-ids'
+import { navigateTo } from '../../lib/deep-linking'
import {
AirDiscountModule,
useGetAirDiscountQuery,
@@ -254,12 +256,21 @@ export const MainHomeScreen: NavigationFunctionComponent = ({
const keyExtractor = useCallback((item: ListItem) => item.id, [])
const scrollY = useRef(new Animated.Value(0)).current
+ const isAppUpdateRequired = useCallback(async () => {
+ const needsUpdate = await needsToUpdateAppVersion()
+ if (needsUpdate) {
+ navigateTo('/update-app', { closable: false })
+ }
+ }, [])
+
useEffect(() => {
// Sync push tokens and unseen notifications
syncToken()
checkUnseen()
// Get user locale from server
getAndSetLocale()
+ // Check if upgrade wall should be shown
+ isAppUpdateRequired()
}, [])
const refetch = useCallback(async () => {
diff --git a/apps/native/app/src/screens/passkey/passkey.tsx b/apps/native/app/src/screens/passkey/passkey.tsx
index 16aee81fce55..238648af275b 100644
--- a/apps/native/app/src/screens/passkey/passkey.tsx
+++ b/apps/native/app/src/screens/passkey/passkey.tsx
@@ -15,7 +15,7 @@ import {
} from 'react-native-navigation'
import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks'
import logo from '../../assets/logo/logo-64w.png'
-import illustrationSrc from '../../assets/illustrations/digital-services-m1.png'
+import illustrationSrc from '../../assets/illustrations/digital-services-m1-dots.png'
import { openNativeBrowser } from '../../lib/rn-island'
import { preferencesStore } from '../../stores/preferences-store'
import { useRegisterPasskey } from '../../lib/passkeys/useRegisterPasskey'
@@ -31,6 +31,22 @@ const Text = styled.View`
margin-top: ${({ theme }) => theme.spacing[5]}px;
`
+const Host = styled.View`
+ justify-content: center;
+ align-items: center;
+ flex: 1;
+`
+
+const ButtonWrapper = styled.View`
+ padding-horizontal: ${({ theme }) => theme.spacing[2]}px;
+ padding-vertical: ${({ theme }) => theme.spacing[4]}px;
+`
+
+const Title = styled(Typography)`
+ padding-horizontal: ${({ theme }) => theme.spacing[2]}px;
+ margin-bottom: ${({ theme }) => theme.spacing[2]}px;
+`
+
const LoadingOverlay = styled.View`
flex: 1;
justify-content: center;
@@ -83,32 +99,19 @@ export const PasskeyScreen: NavigationFunctionComponent<{
style={{ marginHorizontal: 16 }}
/>
-
+
-
+
-
+
-
-
+
+
-
+
{isLoading && (
diff --git a/apps/native/app/src/screens/update-app/update-app.tsx b/apps/native/app/src/screens/update-app/update-app.tsx
new file mode 100644
index 000000000000..9f9b06863c1c
--- /dev/null
+++ b/apps/native/app/src/screens/update-app/update-app.tsx
@@ -0,0 +1,143 @@
+import { Button, Typography, NavigationBarSheet } from '@ui'
+import React, { useEffect } from 'react'
+import { useIntl, FormattedMessage } from 'react-intl'
+import { View, Image, SafeAreaView, Linking } from 'react-native'
+import styled from 'styled-components/native'
+import {
+ Navigation,
+ NavigationFunctionComponent,
+} from 'react-native-navigation'
+import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks'
+import logo from '../../assets/logo/logo-64w.png'
+import illustrationSrc from '../../assets/illustrations/digital-services-m1-dots.png'
+import { isIos } from '../../utils/devices'
+import { preferencesStore } from '../../stores/preferences-store'
+
+const Text = styled.View`
+ margin-horizontal: ${({ theme }) => theme.spacing[7]}px;
+ text-align: center;
+ margin-vertical: ${({ theme }) => theme.spacing[5]}px;
+`
+
+const Host = styled.View`
+ justify-content: center;
+ align-items: center;
+ flex: 1;
+`
+
+const ButtonWrapper = styled.View`
+ padding-horizontal: ${({ theme }) => theme.spacing[2]}px;
+ padding-vertical: ${({ theme }) => theme.spacing[4]}px;
+ gap: ${({ theme }) => theme.spacing[1]}px;
+`
+
+const Title = styled(Typography)`
+ padding-horizontal: ${({ theme }) => theme.spacing[2]}px;
+ margin-bottom: ${({ theme }) => theme.spacing[2]}px;
+`
+
+const { getNavigationOptions, useNavigationOptions } =
+ createNavigationOptionHooks(() => ({
+ topBar: {
+ visible: false,
+ },
+ hardwareBackButton: {
+ dismissModalOnPress: false,
+ },
+ }))
+
+export const UpdateAppScreen: NavigationFunctionComponent<{
+ closable?: boolean
+}> = ({ closable = true, componentId }) => {
+ useNavigationOptions(componentId)
+ const intl = useIntl()
+
+ useEffect(() => {
+ // Make sure to allow closing of the modal if this is a closable screen
+ Navigation.mergeOptions(componentId, {
+ hardwareBackButton: {
+ dismissModalOnPress: closable,
+ },
+ modal: {
+ swipeToDismiss: closable,
+ },
+ })
+ }, [])
+
+ return (
+
+ {
+ if (closable) {
+ preferencesStore.setState({ skippedSoftUpdate: true })
+ Navigation.dismissModal(componentId)
+ }
+ }}
+ style={{ marginHorizontal: 16 }}
+ closable={closable}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+UpdateAppScreen.options = getNavigationOptions
diff --git a/apps/native/app/src/stores/preferences-store.ts b/apps/native/app/src/stores/preferences-store.ts
index b931fca7d184..8d46feec7c54 100644
--- a/apps/native/app/src/stores/preferences-store.ts
+++ b/apps/native/app/src/stores/preferences-store.ts
@@ -32,6 +32,7 @@ export interface PreferencesStore extends State {
vehiclesWidgetEnabled: boolean
airDiscountWidgetEnabled: boolean
widgetsInitialised: boolean
+ skippedSoftUpdate: boolean
lastUsedPasskey: number
notificationsNewDocuments: boolean
notificationsAppUpdates: boolean
@@ -71,6 +72,7 @@ const defaultPreferences = {
vehiclesWidgetEnabled: true,
airDiscountWidgetEnabled: true,
widgetsInitialised: false,
+ skippedSoftUpdate: false,
lastUsedPasskey: 0,
notificationsNewDocuments: true,
notificationsAppUpdates: true,
diff --git a/apps/native/app/src/ui/lib/navigation-bar-sheet/navigation-bar-sheet.tsx b/apps/native/app/src/ui/lib/navigation-bar-sheet/navigation-bar-sheet.tsx
index 82932b08a400..5bc8f39493e7 100644
--- a/apps/native/app/src/ui/lib/navigation-bar-sheet/navigation-bar-sheet.tsx
+++ b/apps/native/app/src/ui/lib/navigation-bar-sheet/navigation-bar-sheet.tsx
@@ -72,12 +72,14 @@ export function NavigationBarSheet({
onClosePress,
style,
showLoading,
+ closable = true,
}: {
title?: React.ReactNode
componentId: string
onClosePress(): void
style?: ViewStyle
showLoading?: boolean
+ closable?: boolean
}) {
const isConnected = useOfflineStore(({ isConnected }) => isConnected)
const wd = useWindowDimensions()
@@ -90,38 +92,42 @@ export function NavigationBarSheet({
return (
<>
- {isHandle && }
+ {isHandle && closable && }
-
- {typeof title === 'string' ? (
- {title}
- ) : (
- title
- )}
-
- {/*Only show loading icon if connected*/}
- {showLoading && isConnected ? : null}
-
-
-
-
-
-
+ {(closable || title) && (
+
+ {typeof title === 'string' ? (
+ {title}
+ ) : (
+ title
+ )}
+
+ {/*Only show loading icon if connected*/}
+ {showLoading && isConnected ? : null}
+
+ {closable && (
+
+
+
+ )}
+
+
+ )}
>
)
diff --git a/apps/native/app/src/utils/component-registry.ts b/apps/native/app/src/utils/component-registry.ts
index 2b695b608411..ba836e520b01 100644
--- a/apps/native/app/src/utils/component-registry.ts
+++ b/apps/native/app/src/utils/component-registry.ts
@@ -47,13 +47,14 @@ export const ComponentRegistry = {
FinanceStatusDetailScreen: `${prefix}.screens.FinanceStatusDetailScreen`,
InboxFilterScreen: `${prefix}.screens.InboxFilterScreen`,
AirDiscountScreen: `${prefix}.screens.AirDiscountScreen`,
+ PasskeyScreen: `${prefix}.screens.PasskeyScreen`,
+ UpdateAppScreen: `${prefix}.screens.UpdateAppScreen`,
// custom navigation icons
OfflineIcon: `${prefix}.navigation.OfflineIcon`,
// overlays
OfflineBanner: `${prefix}.overlay.OfflineBanner`,
- PasskeyScreen: `${prefix}.screens.PasskeyScreen`,
}
export const ButtonRegistry = {
diff --git a/apps/native/app/src/utils/lifecycle/setup-components.tsx b/apps/native/app/src/utils/lifecycle/setup-components.tsx
index 06e456cab432..c4fc85882f9d 100644
--- a/apps/native/app/src/utils/lifecycle/setup-components.tsx
+++ b/apps/native/app/src/utils/lifecycle/setup-components.tsx
@@ -25,6 +25,7 @@ import { LoginScreen } from '../../screens/login/login'
import { TestingLoginScreen } from '../../screens/login/testing-login'
import { MoreScreen } from '../../screens/more/more'
import { PasskeyScreen } from '../../screens/passkey/passkey'
+import { UpdateAppScreen } from '../../screens/update-app/update-app'
import { PersonalInfoScreen } from '../../screens/more/personal-info'
import { NotificationsScreen } from '../../screens/notifications/notifications'
import { OnboardingBiometricsScreen } from '../../screens/onboarding/onboarding-biometrics'
@@ -103,6 +104,7 @@ export function registerAllComponents() {
registerComponent(CR.InboxFilterScreen, InboxFilterScreen)
registerComponent(CR.AirDiscountScreen, AirDiscountScreen)
registerComponent(CR.PasskeyScreen, PasskeyScreen)
+ registerComponent(CR.UpdateAppScreen, UpdateAppScreen)
registerComponent(CR.HomeOptionsScreen, HomeOptionsScreen)
registerComponent(CR.ApplicationsCompletedScreen, ApplicationsCompletedScreen)
registerComponent(
diff --git a/apps/native/app/src/utils/lifecycle/setup-routes.ts b/apps/native/app/src/utils/lifecycle/setup-routes.ts
index a3efb5a76748..f179cf0ff79d 100644
--- a/apps/native/app/src/utils/lifecycle/setup-routes.ts
+++ b/apps/native/app/src/utils/lifecycle/setup-routes.ts
@@ -180,6 +180,26 @@ export function setupRoutes() {
})
})
+ addRoute('/update-app', async (passProps) => {
+ Navigation.showModal({
+ stack: {
+ options: {
+ modal: {
+ swipeToDismiss: false,
+ },
+ },
+ children: [
+ {
+ component: {
+ name: ComponentRegistry.UpdateAppScreen,
+ passProps,
+ },
+ },
+ ],
+ },
+ })
+ })
+
addRoute('/settings', async (passProps) => {
Navigation.showModal({
stack: {
diff --git a/apps/native/app/src/utils/minimum-app-version.ts b/apps/native/app/src/utils/minimum-app-version.ts
new file mode 100644
index 000000000000..db710cadf076
--- /dev/null
+++ b/apps/native/app/src/utils/minimum-app-version.ts
@@ -0,0 +1,19 @@
+import DeviceInfo from 'react-native-device-info'
+import compareVersions from 'compare-versions'
+import { featureFlagClient } from '../contexts/feature-flag-provider'
+
+export const needsToUpdateAppVersion = async (): Promise => {
+ const minimumVersionSupported = await featureFlagClient?.getValueAsync(
+ 'minimumSupportedAppVersion',
+ '1.0.0',
+ )
+
+ if (!minimumVersionSupported) {
+ return false
+ }
+
+ const currentVersion = DeviceInfo.getVersion()
+
+ // @example compare('2.0.0', '1.5.0', '>') => true (update needed)
+ return compareVersions.compare(minimumVersionSupported, currentVersion, '>')
+}
diff --git a/apps/native/app/src/utils/user-agent.ts b/apps/native/app/src/utils/user-agent.ts
new file mode 100644
index 000000000000..977da4e1aceb
--- /dev/null
+++ b/apps/native/app/src/utils/user-agent.ts
@@ -0,0 +1,10 @@
+import { Platform } from 'react-native'
+import DeviceInfo from 'react-native-device-info'
+
+export const getCustomUserAgent = () => {
+ return [
+ `IslandIsApp (${DeviceInfo.getVersion()})`,
+ `Build/${DeviceInfo.getBuildNumber()}`,
+ `(${Platform.OS}/${Platform.Version})`,
+ ].join(' ')
+}
diff --git a/yarn.lock b/yarn.lock
index 911d3842134f..a448c7be1a3d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12539,6 +12539,7 @@ __metadata:
babel-loader: ^8.3.0
babel-plugin-formatjs: 10.3.9
babel-plugin-module-resolver: 5.0.2
+ compare-versions: 6.1.1
configcat-js: 7.0.0
dynamic-color: 0.3.0
expo: 51.0.25
@@ -27179,6 +27180,13 @@ __metadata:
languageName: node
linkType: hard
+"compare-versions@npm:6.1.1":
+ version: 6.1.1
+ resolution: "compare-versions@npm:6.1.1"
+ checksum: 73fe6c4f52d22efe28f0a3be10df2afd704e10b3593360cd963e86b33a7a263c263b41a1361b69c30a0fe68bfa70fef90860c1cf2ef41502629d4402890fcd57
+ languageName: node
+ linkType: hard
+
"component-emitter@npm:^1.2.0, component-emitter@npm:^1.3.0":
version: 1.3.0
resolution: "component-emitter@npm:1.3.0"