diff --git a/projects/Mallard/src/AppNavigation.tsx b/projects/Mallard/src/AppNavigation.tsx
index 7e4300b3d..5b6dcdc86 100644
--- a/projects/Mallard/src/AppNavigation.tsx
+++ b/projects/Mallard/src/AppNavigation.tsx
@@ -1,5 +1,5 @@
-import { NavigationContainer } from '@react-navigation/native';
import type { NavigationContainerRef } from '@react-navigation/native';
+import { NavigationContainer } from '@react-navigation/native';
import type { StackCardInterpolationProps } from '@react-navigation/stack';
import {
CardStyleInterpolators,
@@ -9,6 +9,7 @@ import {
import React, { useRef } from 'react';
import { Animated } from 'react-native';
import { isTablet } from 'react-native-device-info';
+import { IAPAppMigrationModal } from './components/Modals/IAPAppMigration';
import {
MissingIAPRestoreError,
MissingIAPRestoreMissing,
@@ -366,6 +367,16 @@ const MainStack = () => {
},
}}
/>
+
);
diff --git a/projects/Mallard/src/components/Modals/IAPAppMigration.tsx b/projects/Mallard/src/components/Modals/IAPAppMigration.tsx
new file mode 100644
index 000000000..8f8a46923
--- /dev/null
+++ b/projects/Mallard/src/components/Modals/IAPAppMigration.tsx
@@ -0,0 +1,31 @@
+import { useNavigation } from '@react-navigation/native';
+import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
+import React from 'react';
+import { logEvent } from '../../helpers/analytics';
+import { hasSeenIapMigrationMessage } from '../../helpers/storage';
+import type { MainStackParamList } from '../../navigation/NavigationModels';
+import { RouteNames } from '../../navigation/NavigationModels';
+import { CenterWrapper } from '../CenterWrapper/CenterWrapper';
+import { IAPAppMigrationModalCard } from '../iap-app-migration-modal-card';
+
+const IAPAppMigrationModal = () => {
+ const { navigate } =
+ useNavigation>();
+ return (
+
+ {
+ navigate(RouteNames.Issue);
+
+ logEvent({
+ name: 'iap_app_migration_modal',
+ value: 'iap_app_migration_modal_dismissed',
+ });
+ hasSeenIapMigrationMessage.set(true);
+ }}
+ />
+
+ );
+};
+
+export { IAPAppMigrationModal };
diff --git a/projects/Mallard/src/components/iap-app-migration-modal-card.tsx b/projects/Mallard/src/components/iap-app-migration-modal-card.tsx
new file mode 100644
index 000000000..8db24a91e
--- /dev/null
+++ b/projects/Mallard/src/components/iap-app-migration-modal-card.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import { copy } from '../helpers/words';
+import { color } from '../theme/color';
+import { CardAppearance, OnboardingCard } from './onboarding/onboarding-card';
+import { UiBodyCopy } from './styled-text';
+
+const style = StyleSheet.create({
+ bodyCopy: {
+ color: color.palette.neutral[100],
+ },
+});
+
+const IAPAppMigrationModalCard = ({ onDismiss }: { onDismiss: () => void }) => {
+ return (
+
+ {copy.iAPMigration.body}
+
+ }
+ />
+ );
+};
+
+export { IAPAppMigrationModalCard };
diff --git a/projects/Mallard/src/components/onboarding/onboarding-card.tsx b/projects/Mallard/src/components/onboarding/onboarding-card.tsx
index c19b71fe4..8004c9b54 100644
--- a/projects/Mallard/src/components/onboarding/onboarding-card.tsx
+++ b/projects/Mallard/src/components/onboarding/onboarding-card.tsx
@@ -13,6 +13,7 @@ export enum CardAppearance {
Tomato,
Apricot,
Blue,
+ Clashy,
}
const styles = StyleSheet.create({
@@ -76,6 +77,11 @@ const appearances: {
titleText: { color: color.palette.neutral[100] },
subtitleText: { color: color.primary },
}),
+ [CardAppearance.Clashy]: StyleSheet.create({
+ background: { backgroundColor: color.ui.sea },
+ titleText: { color: color.ui.brightYellow },
+ subtitleText: { color: color.palette.neutral[100] },
+ }),
};
const OnboardingCard = ({
@@ -144,7 +150,11 @@ const OnboardingCard = ({
onPress={onDismissThisCard}
accessibilityHint="This will dismiss the onboarding card"
accessibilityLabel={`Dismiss the ${title} onboarding card`}
- appearance={ButtonAppearance.SkeletonBlue}
+ appearance={
+ appearance === CardAppearance.Clashy
+ ? ButtonAppearance.SkeletonLight
+ : ButtonAppearance.SkeletonBlue
+ }
/>
)}
diff --git a/projects/Mallard/src/helpers/storage.ts b/projects/Mallard/src/helpers/storage.ts
index 1dc875761..1cfdd9e42 100644
--- a/projects/Mallard/src/helpers/storage.ts
+++ b/projects/Mallard/src/helpers/storage.ts
@@ -130,6 +130,10 @@ const hasShownRatingCache = createAsyncCache(
'@Setting_hasShownRating',
);
+const hasSeenIapMigrationMessage = createAsyncCache(
+ '@Setting_hasSeenIapMigrationMessage',
+);
+
const issueSummaryCache = createAsyncCache('issueSummary');
/**
* Creates a simple store (wrapped around the keychain) for tokens.
@@ -209,4 +213,5 @@ export {
hasShownRatingCache,
oktaDataCache,
issueSummaryCache,
+ hasSeenIapMigrationMessage,
};
diff --git a/projects/Mallard/src/helpers/words.ts b/projects/Mallard/src/helpers/words.ts
index 4ee79c0cd..1ae0461c7 100644
--- a/projects/Mallard/src/helpers/words.ts
+++ b/projects/Mallard/src/helpers/words.ts
@@ -250,6 +250,11 @@ const enableAll = 'Enable all';
const rejectAll = 'Reject all';
const andContinue = 'and continue';
+const iAPMigration = {
+ title: 'The app is changing...',
+ body: 'On 16/12/2024, we will be switching to a new app. Your subscription will be transferred automatically, so you don’t need to do anything. The new app will feel more like a digital version of our newspaper, and we hope it delivers an intuitive reading experience you continue to enjoy.\n\n Thank you for your ongoing support.',
+};
+
export const copy = {
alreadySubscribed,
andContinue,
@@ -259,6 +264,7 @@ export const copy = {
externalSubscription,
failedSignIn,
homeScreen,
+ iAPMigration,
issueListFooter,
manageDownloads,
newEditionWords,
diff --git a/projects/Mallard/src/navigation/NavigationModels.tsx b/projects/Mallard/src/navigation/NavigationModels.tsx
index 27b56bb2f..ebd221bfb 100644
--- a/projects/Mallard/src/navigation/NavigationModels.tsx
+++ b/projects/Mallard/src/navigation/NavigationModels.tsx
@@ -49,6 +49,7 @@ export type MainStackParamList = {
OnboardingConsent: undefined;
PrivacyPolicyInline: undefined;
OnboardingConsentInline: undefined;
+ IAPAppMigrationModal: undefined;
};
export enum RouteNames {
@@ -86,4 +87,5 @@ export enum RouteNames {
MissingIAPRestoreError = 'MissingIAPRestoreError',
MissingIAPRestoreMissing = 'MissingIAPRestoreMissing',
ManageEditionsFromSettings = 'ManageEditionsFromSettings',
+ IAPAppMigrationModal = 'IAPAppMigrationModal',
}
diff --git a/projects/Mallard/src/screens/issue-screen.tsx b/projects/Mallard/src/screens/issue-screen.tsx
index 7a28c1ed8..8385346fc 100644
--- a/projects/Mallard/src/screens/issue-screen.tsx
+++ b/projects/Mallard/src/screens/issue-screen.tsx
@@ -1,18 +1,26 @@
import { useNavigation, useRoute } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { MutableRefObject, ReactElement } from 'react';
-import React, { useCallback, useEffect, useMemo, useRef } from 'react';
+import React, {
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
import type { StyleProp, ViewStyle } from 'react-native';
import { FlatList, StyleSheet, View } from 'react-native';
import Image from 'react-native-fast-image';
import RNRestart from 'react-native-restart';
import SplashScreen from 'react-native-splash-screen';
-import { PageLayoutSizes } from '../common';
+import { AccessContext } from '../authentication/AccessContext';
import type {
IssueWithFronts,
SpecialEditionHeaderStyles,
Front as TFront,
} from '../common';
+import { PageLayoutSizes } from '../common';
import { ReloadButton } from '../components/Button/ReloadButton';
import { Front } from '../components/front';
import { FlexCenter } from '../components/layout/flex-center';
@@ -30,6 +38,7 @@ import {
} from '../components/weather';
import { deleteIssueFiles } from '../download-edition/clear-issues-and-editions';
import { logPageView } from '../helpers/analytics';
+import { hasSeenIapMigrationMessage } from '../helpers/storage';
import type { FlatCard } from '../helpers/transform';
import {
flattenCollectionsToCards,
@@ -61,6 +70,7 @@ import {
type MainStackParamList,
RouteNames,
} from '../navigation/NavigationModels';
+import { remoteConfigService } from '../services/remote-config';
import { Breakpoints } from '../theme/breakpoints';
import { metrics } from '../theme/spacing';
import { SLIDER_FRONT_HEIGHT } from './article/slider/SliderTitle';
@@ -442,6 +452,16 @@ export const IssueScreen = React.memo(() => {
const { hasSetGdpr } = useGdprSettings();
const { navigate } =
useNavigation>();
+ const { iapData } = useContext(AccessContext);
+
+ useEffect(() => {
+ hasSeenIapMigrationMessage.get().then((hasSeen) => {
+ !hasSeen &&
+ iapData &&
+ remoteConfigService.getBoolean('is_iap_message_enabled') &&
+ navigate(RouteNames.IAPAppMigrationModal);
+ });
+ }, []);
// This is only returning true or false so keep reverting. Need an onboarding state
const isOnboarded = hasSetGdpr();
diff --git a/projects/Mallard/src/screens/settings/dev-zone.tsx b/projects/Mallard/src/screens/settings/dev-zone.tsx
index ab3473ab7..8672d6224 100644
--- a/projects/Mallard/src/screens/settings/dev-zone.tsx
+++ b/projects/Mallard/src/screens/settings/dev-zone.tsx
@@ -19,6 +19,7 @@ import { locale } from '../../helpers/locale';
import { isInBeta, isInTestFlight } from '../../helpers/release-stream';
import { imageForScreenSize } from '../../helpers/screen';
import {
+ hasSeenIapMigrationMessage,
issueSummaryCache,
pushRegisteredTokens,
showAllEditionsCache,
@@ -243,6 +244,13 @@ const DevZone = () => {
Add legacy IAP receipt
)}
+