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 )} +