From 3255500da762ee366f017d401767e66d5b0b68d6 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 28 Jun 2024 08:16:55 +0200 Subject: [PATCH 01/20] add MenuItem and CalendarSolid icon --- assets/images/calendar-solid.svg | 7 ++++ src/components/Icon/Expensicons.ts | 2 ++ .../Subscription/CardSection/CardSection.tsx | 32 +++++++++++++++++-- 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 assets/images/calendar-solid.svg diff --git a/assets/images/calendar-solid.svg b/assets/images/calendar-solid.svg new file mode 100644 index 000000000000..168aabaa4a49 --- /dev/null +++ b/assets/images/calendar-solid.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 3b6d51e786a3..7ef74fbe5633 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -27,6 +27,7 @@ import Bolt from '@assets/images/bolt.svg'; import Briefcase from '@assets/images/briefcase.svg'; import Bug from '@assets/images/bug.svg'; import Building from '@assets/images/building.svg'; +import CalendarSolid from '@assets/images/calendar-solid.svg'; import Calendar from '@assets/images/calendar.svg'; import Camera from '@assets/images/camera.svg'; import CarWithKey from '@assets/images/car-with-key.svg'; @@ -368,4 +369,5 @@ export { CheckCircle, CheckmarkCircle, NetSuiteSquare, + CalendarSolid, }; diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 1cb9d7b1d619..958aa7bff280 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -1,9 +1,11 @@ -import React, {useMemo} from 'react'; +import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; +import Onyx, {useOnyx} from 'react-native-onyx'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; +import * as Illustrations from '@components/Icon/Illustrations'; import MenuItem from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; @@ -37,6 +39,25 @@ function CardSection() { const sectionSubtitle = defaultCard && !!nextPaymentDate ? translate('subscription.cardSection.cardNextPayment', {nextPaymentDate}) : translate('subscription.cardSection.subtitle'); const BillingBanner = ; + useEffect(() => { + Onyx.merge(ONYXKEYS.FUND_LIST, [ + { + accountData: { + cardMonth: 11, + + cardNumber: '1234', + + cardYear: 2026, + + currency: 'USD', + + addressName: 'John Doe', + }, + isDefault: true, + }, + ]); + }, [fundList]); + return (
} + {!!account?.hasPurchases && ( Date: Mon, 1 Jul 2024 13:24:54 +0200 Subject: [PATCH 02/20] add RequestEarlyCancellationPage to navigator --- src/ROUTES.ts | 1 + src/SCREENS.ts | 1 + .../ModalStackNavigators/index.tsx | 1 + .../CENTRAL_PANE_TO_RHP_MAPPING.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 ++ .../Subscription/CardSection/CardSection.tsx | 19 ++++---- .../index.native.tsx | 19 ++++++++ .../RequestEarlyCancellationPage/index.tsx | 46 +++++++++++++++++++ 8 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 src/pages/settings/Subscription/RequestEarlyCancellationPage/index.native.tsx create mode 100644 src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 33eb78dc300d..1cc7365c8431 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -111,6 +111,7 @@ const ROUTES = { SETTINGS_SUBSCRIPTION_CHANGE_BILLING_CURRENCY: 'settings/subscription/change-billing-currency', SETTINGS_SUBSCRIPTION_CHANGE_PAYMENT_CURRENCY: 'settings/subscription/add-payment-card/change-payment-currency', SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY: 'settings/subscription/disable-auto-renew-survey', + SETTINGS_SUBSCRIPTION_REQUEST_EARLY_CANCELLATION: 'settings/subscription/request-early-cancellation', SETTINGS_PRIORITY_MODE: 'settings/preferences/priority-mode', SETTINGS_LANGUAGE: 'settings/preferences/language', SETTINGS_THEME: 'settings/preferences/theme', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 1807c9bb0bab..d0a0e3094838 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -107,6 +107,7 @@ const SCREENS = { DISABLE_AUTO_RENEW_SURVEY: 'Settings_Subscription_DisableAutoRenewSurvey', CHANGE_BILLING_CURRENCY: 'Settings_Subscription_Change_Billing_Currency', CHANGE_PAYMENT_CURRENCY: 'Settings_Subscription_Change_Payment_Currency', + REQUEST_EARLY_CANCELLATION: 'Settings_Subscription_RequestEarlyCancellation', }, }, SAVE_THE_WORLD: { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 81ecb85299da..59468b03743e 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -216,6 +216,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/CustomStatus/SetTimePage').default, [SCREENS.SETTINGS.SUBSCRIPTION.SIZE]: () => require('../../../../pages/settings/Subscription/SubscriptionSize').default, [SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY]: () => require('../../../../pages/settings/Subscription/DisableAutoRenewSurveyPage').default, + [SCREENS.SETTINGS.SUBSCRIPTION.REQUEST_EARLY_CANCELLATION]: () => require('../../../../pages/settings/Subscription/RequestEarlyCancellationPage').default, [SCREENS.WORKSPACE.RATE_AND_UNIT]: () => require('../../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage/InitialPage').default, [SCREENS.WORKSPACE.RATE_AND_UNIT_RATE]: () => require('../../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage').default, [SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT]: () => require('../../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage').default, diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index 1192e4649ea0..cf260a8f6d54 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -43,6 +43,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD, SCREENS.SETTINGS.SUBSCRIPTION.SIZE, SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY, + SCREENS.SETTINGS.SUBSCRIPTION.REQUEST_EARLY_CANCELLATION, SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY, SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY, ], diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index a67b90beb04e..08bd25026adf 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -292,6 +292,9 @@ const config: LinkingOptions['config'] = { [SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY]: { path: ROUTES.SETTINGS_SUBSCRIPTION_DISABLE_AUTO_RENEW_SURVEY, }, + [SCREENS.SETTINGS.SUBSCRIPTION.REQUEST_EARLY_CANCELLATION]: { + path: ROUTES.SETTINGS_SUBSCRIPTION_REQUEST_EARLY_CANCELLATION, + }, [SCREENS.WORKSPACE.CURRENCY]: { path: ROUTES.WORKSPACE_PROFILE_CURRENCY.route, }, diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 17249be93898..710e9fd19e4d 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -3,9 +3,7 @@ import {View} from 'react-native'; import Onyx, {useOnyx} from 'react-native-onyx'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import * as Illustrations from '@components/Icon/Illustrations'; import MenuItem from '@components/MenuItem'; -import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; @@ -93,13 +91,16 @@ function CardSection() { )} {isEmptyObject(defaultCard?.accountData) && } - + {!!privateSubscription?.type && ( + Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_REQUEST_EARLY_CANCELLATION)} + /> + )} {!!account?.hasPurchases && ( + + + ); +} + +RequestEarlyCancellationPage.displayName = 'RequestEarlyCancellationPage'; + +export default RequestEarlyCancellationPage; diff --git a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx new file mode 100644 index 000000000000..dcec77b1a0ff --- /dev/null +++ b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import FeedbackSurvey from '@components/FeedbackSurvey'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import * as Subscription from '@userActions/Subscription'; +import type {FeedbackSurveyOptionID} from '@src/CONST'; + +function RequestEarlyCancellationPage() { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const handleSubmit = (key: FeedbackSurveyOptionID, additionalNote?: string) => { + Subscription.updateSubscriptionAutoRenew(false, key, additionalNote); + Navigation.goBack(); + }; + + return ( + + + + + + + ); +} + +RequestEarlyCancellationPage.displayName = 'RequestEarlyCancellationPage'; + +export default RequestEarlyCancellationPage; From 6b8ffab0229ae1b30e548195c71af756035f2ba5 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 1 Jul 2024 14:25:35 +0200 Subject: [PATCH 03/20] add translations to first step --- src/components/FeedbackSurvey.tsx | 7 ++++++- src/languages/en.ts | 20 ++++++++++++++++++ src/languages/es.ts | 21 +++++++++++++++++++ .../RequestEarlyCancellationPage/index.tsx | 19 +++++++++++++++-- 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/components/FeedbackSurvey.tsx b/src/components/FeedbackSurvey.tsx index a7b0732be1fb..b689c5643c3a 100644 --- a/src/components/FeedbackSurvey.tsx +++ b/src/components/FeedbackSurvey.tsx @@ -25,6 +25,9 @@ type FeedbackSurveyProps = { /** Styles for the option row element */ optionRowStyles?: StyleProp; + + /** Optional text to render over the submit button */ + bottomText?: React.ReactNode; }; type Option = { @@ -39,7 +42,7 @@ const OPTIONS: Option[] = [ {key: CONST.FEEDBACK_SURVEY_OPTIONS.BUSINESS_CLOSING.ID, label: CONST.FEEDBACK_SURVEY_OPTIONS.BUSINESS_CLOSING.TRANSLATION_KEY}, ]; -function FeedbackSurvey({title, description, onSubmit, optionRowStyles}: FeedbackSurveyProps) { +function FeedbackSurvey({title, description, onSubmit, optionRowStyles, bottomText}: FeedbackSurveyProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); @@ -89,12 +92,14 @@ function FeedbackSurvey({title, description, onSubmit, optionRowStyles}: Feedbac )} + {!!bottomText && bottomText} diff --git a/src/languages/en.ts b/src/languages/en.ts index 095cc12a3896..80065f5c2ae9 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3428,6 +3428,26 @@ export default { whatsMainReason: "What's the main reason you're disabling auto-renew?", renewsOn: ({date}) => `Renews on ${date}.`, }, + requestEarlyCancellation: { + title: 'Request early cancellation', + subtitle: 'What’s the main reason you’re requesting early cancellation?', + subscriptionCancelled: { + title: 'Subscription canceled', + subtitle: 'Your annual subscription has been canceled.', + info1: 'If you want to keep using your workspace(s) on a pay-per-use basis, you’re all set.', + info2: 'If you’d like to prevent future activity and charges, you must delete your workspace(s). Note that when you delete your workspace(s), you’ll be charged for any outstanding activity that was incurred during the current calendar month.', + }, + requestSubmitted: { + title: 'Request submitted', + subtitle: 'Thanks for letting us know you’re interested in canceling your subscription. We’re reviewing your request and will be in touch soon via your chat with Concierge.', + }, + submitButton: 'Done', + acknowledgement: { + part1: 'By requesting early cancellation, I acknowledge and agree that Expensify has no obligation to grant such request under the Expensify ', + link: 'Terms of Service', + part2: ' or other applicable services agreement between me and Expensify and that Expensify retains sole discretion with regard to granting any such request.', + }, + }, }, feedbackSurvey: { tooLimited: 'Functionality needs improvement', diff --git a/src/languages/es.ts b/src/languages/es.ts index e2a00222c62a..e3374dd30034 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3928,6 +3928,27 @@ export default { whatsMainReason: '¿Cuál es la razón principal por la que deseas desactivar la auto-renovación?', renewsOn: ({date}) => `Se renovará el ${date}.`, }, + requestEarlyCancellation: { + title: 'Solicitar cancelación anticipada', + subtitle: 'What’s the main reason you’re requesting early cancellation?', + subscriptionCancelled: { + title: 'Suscripción cancelada', + subtitle: 'Tu suscripción anual ha sido cancelada.', + info1: 'Ya puedes seguir utilizando tu(s) espacio(s) de trabajo en la modalidad de pago por uso.', + info2: 'Si quieres evitar actividad y cargos futuros, debes eliminar tu(s) espacio(s) de trabajo. Ten en cuenta que cuando elimines tu(s) espacio(s) de trabajo, se te cobrará cualquier actividad pendienteque se haya incurrido durante el mes en curso.', + }, + requestSubmitted: { + title: 'Solicitud enviada', + subtitle: + 'Gracias por hacernos saber que deseas cancelar tu suscripción. Estamos revisando tu solicitud y nos comunicaremos contigo en breve a través de tu chat con Concierge.', + }, + submitButton: 'Listo', + acknowledgement: { + part1: 'By requesting early cancellation, I acknowledge and agree that Expensify has no obligation to grant such request under the Expensify ', + link: 'Terms of Service', + part2: ' or other applicable services agreement between me and Expensify and that Expensify retains sole discretion with regard to granting any such request.', + }, + }, }, feedbackSurvey: { tooLimited: 'Hay que mejorar la funcionalidad', diff --git a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx index dcec77b1a0ff..41fab53f4d33 100644 --- a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx +++ b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx @@ -3,6 +3,7 @@ import FeedbackSurvey from '@components/FeedbackSurvey'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; @@ -18,6 +19,19 @@ function RequestEarlyCancellationPage() { Navigation.goBack(); }; + const acknowledgmentText = ( + + {translate('subscription.requestEarlyCancellation.acknowledgement.part1')} + {}} + > + {translate('subscription.requestEarlyCancellation.acknowledgement.link')} + + {translate('subscription.requestEarlyCancellation.acknowledgement.part2')} + + ); + return ( {acknowledgmentText}} /> From 42c47a550e6e2e91712a74557f33ce982a3a1e01 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 2 Jul 2024 15:10:07 +0200 Subject: [PATCH 04/20] create views for automatic and manual cancellation types --- src/CONST.ts | 8 +- src/languages/en.ts | 14 ++- src/languages/es.ts | 23 +++-- .../RequestEarlyCancellationPage/index.tsx | 96 +++++++++++++++---- 4 files changed, 108 insertions(+), 33 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 233b35e6ac4b..e0860d3370db 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4939,6 +4939,11 @@ const CONST = { }, EXCLUDE_FROM_LAST_VISITED_PATH: [SCREENS.NOT_FOUND, SCREENS.SAML_SIGN_IN, SCREENS.VALIDATE_LOGIN] as string[], + + CANCELLATION_TYPE: { + MANUAL: 'manual', + AUTOMATIC: 'automatic', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; @@ -4949,7 +4954,8 @@ type IOURequestType = ValueOf; type FeedbackSurveyOptionID = ValueOf, 'ID'>>; type SubscriptionType = ValueOf; +type CancellationType = ValueOf; -export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType, SubscriptionType, FeedbackSurveyOptionID}; +export type {Country, IOUAction, IOUType, RateAndUnit, OnboardingPurposeType, IOURequestType, SubscriptionType, FeedbackSurveyOptionID, CancellationType}; export default CONST; diff --git a/src/languages/en.ts b/src/languages/en.ts index 80065f5c2ae9..400c66de556d 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3434,12 +3434,20 @@ export default { subscriptionCancelled: { title: 'Subscription canceled', subtitle: 'Your annual subscription has been canceled.', - info1: 'If you want to keep using your workspace(s) on a pay-per-use basis, you’re all set.', - info2: 'If you’d like to prevent future activity and charges, you must delete your workspace(s). Note that when you delete your workspace(s), you’ll be charged for any outstanding activity that was incurred during the current calendar month.', + info: 'If you want to keep using your workspace(s) on a pay-per-use basis, you’re all set.', + preventFutureActivity: { + part1: 'If you’d like to prevent future activity and charges, you must ', + link: 'delete your workspace(s)', + part2: '. Note that when you delete your workspace(s), you’ll be charged for any outstanding activity that was incurred during the current calendar month.', + }, }, requestSubmitted: { title: 'Request submitted', - subtitle: 'Thanks for letting us know you’re interested in canceling your subscription. We’re reviewing your request and will be in touch soon via your chat with Concierge.', + subtitle: { + part1: 'Thanks for letting us know you’re interested in canceling your subscription. We’re reviewing your request and will be in touch soon via your chat with ', + link: 'Concierge', + part2: '.', + }, }, submitButton: 'Done', acknowledgement: { diff --git a/src/languages/es.ts b/src/languages/es.ts index e3374dd30034..48697682c5ff 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3930,23 +3930,30 @@ export default { }, requestEarlyCancellation: { title: 'Solicitar cancelación anticipada', - subtitle: 'What’s the main reason you’re requesting early cancellation?', + subtitle: '¿Cuál es la razón principal por la que solicitas la cancelación anticipada?', subscriptionCancelled: { title: 'Suscripción cancelada', subtitle: 'Tu suscripción anual ha sido cancelada.', - info1: 'Ya puedes seguir utilizando tu(s) espacio(s) de trabajo en la modalidad de pago por uso.', - info2: 'Si quieres evitar actividad y cargos futuros, debes eliminar tu(s) espacio(s) de trabajo. Ten en cuenta que cuando elimines tu(s) espacio(s) de trabajo, se te cobrará cualquier actividad pendienteque se haya incurrido durante el mes en curso.', + info: 'Ya puedes seguir utilizando tu(s) espacio(s) de trabajo en la modalidad de pago por uso.', + preventFutureActivity: { + part1: 'Si quieres evitar actividad y cargos futuros, debes ', + link: 'eliminar tu(s) espacio(s) de trabajo.', + part2: ' Ten en cuenta que cuando elimines tu(s) espacio(s) de trabajo, se te cobrará cualquier actividad pendienteque se haya incurrido durante el mes en curso.', + }, }, requestSubmitted: { title: 'Solicitud enviada', - subtitle: - 'Gracias por hacernos saber que deseas cancelar tu suscripción. Estamos revisando tu solicitud y nos comunicaremos contigo en breve a través de tu chat con Concierge.', + subtitle: { + part1: 'Gracias por hacernos saber que deseas cancelar tu suscripción. Estamos revisando tu solicitud y nos comunicaremos contigo en breve a través de tu chat con ', + link: 'Concierge', + part2: '.', + }, }, submitButton: 'Listo', acknowledgement: { - part1: 'By requesting early cancellation, I acknowledge and agree that Expensify has no obligation to grant such request under the Expensify ', - link: 'Terms of Service', - part2: ' or other applicable services agreement between me and Expensify and that Expensify retains sole discretion with regard to granting any such request.', + part1: 'Al solicitar la cancelación anticipada, reconozco y acepto que Expensify no tiene ninguna obligación de conceder dicha solicitud en virtud de las ', + link: 'Condiciones de Servicio', + part2: ' de Expensify u otro acuerdo de servicios aplicable entre Expensify y yo, y que Expensify se reserva el derecho exclusivo a conceder dicha solicitud.', }, }, }, diff --git a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx index 41fab53f4d33..9298e28778c5 100644 --- a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx +++ b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx @@ -1,37 +1,99 @@ -import React from 'react'; +import React, {useState} from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; import FeedbackSurvey from '@components/FeedbackSurvey'; +import FixedFooter from '@components/FixedFooter'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; +import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import * as Subscription from '@userActions/Subscription'; -import type {FeedbackSurveyOptionID} from '@src/CONST'; +import type {CancellationType, FeedbackSurveyOptionID} from '@src/CONST'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; function RequestEarlyCancellationPage() { const {translate} = useLocalize(); const styles = useThemeStyles(); - const handleSubmit = (key: FeedbackSurveyOptionID, additionalNote?: string) => { - Subscription.updateSubscriptionAutoRenew(false, key, additionalNote); - Navigation.goBack(); + // TODO: replace this with NVP_PRIVATE_CANCELLATION_DETAILS.cancellationType + const [cancellationType, setCancellationType] = useState(CONST.CANCELLATION_TYPE.MANUAL); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const handleSubmit = (cancellationReason: FeedbackSurveyOptionID, cancellationNote = '') => { + // TODO: call the CancelBillingSubscriptionNewDot API method + setCancellationType(CONST.CANCELLATION_TYPE.AUTOMATIC); }; const acknowledgmentText = ( {translate('subscription.requestEarlyCancellation.acknowledgement.part1')} - {}} - > - {translate('subscription.requestEarlyCancellation.acknowledgement.link')} - + {translate('subscription.requestEarlyCancellation.acknowledgement.link')} {translate('subscription.requestEarlyCancellation.acknowledgement.part2')} ); + let screenContent: React.ReactNode; + + if (cancellationType === CONST.CANCELLATION_TYPE.MANUAL) { + screenContent = ( + + + {translate('subscription.requestEarlyCancellation.requestSubmitted.title')} + + {translate('subscription.requestEarlyCancellation.requestSubmitted.subtitle.part1')} + {}}>{translate('subscription.requestEarlyCancellation.requestSubmitted.subtitle.link')} + {translate('subscription.requestEarlyCancellation.requestSubmitted.subtitle.part2')} + + + +
{account?.isEligibleForRefund && ( diff --git a/src/pages/settings/Subscription/CardSection/RequestEarlyCancellationMenuItem/index.tsx b/src/pages/settings/Subscription/CardSection/RequestEarlyCancellationMenuItem/index.tsx index 2b2fe624b02f..9fbb48e51605 100644 --- a/src/pages/settings/Subscription/CardSection/RequestEarlyCancellationMenuItem/index.tsx +++ b/src/pages/settings/Subscription/CardSection/RequestEarlyCancellationMenuItem/index.tsx @@ -18,7 +18,7 @@ function RequestEarlyCancellationMenuItem() { icon={Expensicons.CalendarSolid} iconFill={theme.success} shouldShowRightIcon - wrapperStyle={[styles.sectionMenuItemTopDescription, styles.mt5]} + wrapperStyle={styles.sectionMenuItemTopDescription} onPress={() => Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_REQUEST_EARLY_CANCELLATION)} /> ); From ec5b10d9ed42cec2427b78f2415c819517b3b0ca Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 9 Jul 2024 15:33:00 +0200 Subject: [PATCH 12/20] start backend integration --- src/CONST.ts | 1 + src/ONYXKEYS.ts | 3 ++ .../CancelBillingSubscriptionParams.ts | 8 ++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/Subscription.ts | 47 ++++++++++++++++++- .../RequestEarlyCancellationPage/index.tsx | 16 +++---- src/types/onyx/CancellationDetails.ts | 29 ++++++++++++ src/types/onyx/index.ts | 2 + 9 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 src/libs/API/parameters/CancelBillingSubscriptionParams.ts create mode 100644 src/types/onyx/CancellationDetails.ts diff --git a/src/CONST.ts b/src/CONST.ts index 28b4b5e3804c..deeeb3318650 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5073,6 +5073,7 @@ const CONST = { CANCELLATION_TYPE: { MANUAL: 'manual', AUTOMATIC: 'automatic', + NONE: 'none', }, REPORT_FIELD_TYPES: { TEXT: 'text', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 709347fa71cd..b9feb682694a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -376,6 +376,8 @@ const ONYXKEYS = { /** Stores the information about the state of issuing a new card */ ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard', + NVP_PRIVATE_CANCELLATION_DETAILS: 'nvp_private_cancellationDetails', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -798,6 +800,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_BILLING_FUND_ID]: number; [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number; [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number; + [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/libs/API/parameters/CancelBillingSubscriptionParams.ts b/src/libs/API/parameters/CancelBillingSubscriptionParams.ts new file mode 100644 index 000000000000..252a7420d27d --- /dev/null +++ b/src/libs/API/parameters/CancelBillingSubscriptionParams.ts @@ -0,0 +1,8 @@ +import type {FeedbackSurveyOptionID} from '@src/CONST'; + +type CancelBillingSubscriptionParams = { + cancellationReason: FeedbackSurveyOptionID; + cancellationNote: string; +}; + +export default CancelBillingSubscriptionParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 0b63ec3ed465..6613264fab4d 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -242,3 +242,4 @@ export type {default as CreateWorkspaceReportFieldParams} from './CreateWorkspac export type {default as OpenPolicyExpensifyCardsPageParams} from './OpenPolicyExpensifyCardsPageParams'; export type {default as RequestExpensifyCardLimitIncreaseParams} from './RequestExpensifyCardLimitIncreaseParams'; export type {default as UpdateNetSuiteGenericTypeParams} from './UpdateNetSuiteGenericTypeParams'; +export type {default as CancelBillingSubscriptionParams} from './CancelBillingSubscriptionParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index bd10be8948bc..e1c70f230634 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -254,6 +254,7 @@ const WRITE_COMMANDS = { REQUEST_EXPENSIFY_CARD_LIMIT_INCREASE: 'RequestExpensifyCardLimitIncrease', CONNECT_POLICY_TO_SAGE_INTACCT: 'ConnectPolicyToSageIntacct', CLEAR_OUTSTANDING_BALANCE: 'ClearOutstandingBalance', + CANCEL_BILLING_SUBSCRIPTION: 'CancelBillingSubscriptionNewDot', } as const; type WriteCommand = ValueOf; @@ -459,6 +460,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ENABLE_DISTANCE_REQUEST_TAX]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams; [WRITE_COMMANDS.REQUEST_EXPENSIFY_CARD_LIMIT_INCREASE]: Parameters.RequestExpensifyCardLimitIncreaseParams; [WRITE_COMMANDS.CLEAR_OUTSTANDING_BALANCE]: null; + [WRITE_COMMANDS.CANCEL_BILLING_SUBSCRIPTION]: Parameters.CancelBillingSubscriptionParams; [WRITE_COMMANDS.UPDATE_POLICY_CONNECTION_CONFIG]: Parameters.UpdatePolicyConnectionConfigParams; [WRITE_COMMANDS.UPDATE_MANY_POLICY_CONNECTION_CONFIGS]: Parameters.UpdateManyPolicyConnectionConfigurationsParams; diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index beed2b1b2962..864f92d99d0a 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -1,7 +1,7 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import type {UpdateSubscriptionAddNewUsersAutomaticallyParams, UpdateSubscriptionAutoRenewParams, UpdateSubscriptionTypeParams} from '@libs/API/parameters'; +import type {CancelBillingSubscriptionParams, UpdateSubscriptionAddNewUsersAutomaticallyParams, UpdateSubscriptionAutoRenewParams, UpdateSubscriptionTypeParams} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import CONST from '@src/CONST'; import type {FeedbackSurveyOptionID, SubscriptionType} from '@src/CONST'; @@ -279,6 +279,50 @@ function clearOutstandingBalance() { API.write(WRITE_COMMANDS.CLEAR_OUTSTANDING_BALANCE, null, onyxData); } +function cancelBillingSubscription(cancellationReason: FeedbackSurveyOptionID, cancellationNote: string) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS, + value: { + cancellationReason, + errors: null, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS, + value: { + errors: null, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS, + value: { + cancellationType: null, + }, + }, + ]; + + const parameters: CancelBillingSubscriptionParams = { + cancellationReason, + cancellationNote, + }; + + API.write(WRITE_COMMANDS.CANCEL_BILLING_SUBSCRIPTION, parameters, { + optimisticData, + successData, + failureData, + }); +} + export { openSubscriptionPage, updateSubscriptionAutoRenew, @@ -287,4 +331,5 @@ export { clearUpdateSubscriptionSizeError, updateSubscriptionType, clearOutstandingBalance, + cancelBillingSubscription, }; diff --git a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx index ba9fbac37b69..3324d579e75a 100644 --- a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx +++ b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx @@ -1,5 +1,6 @@ -import React, {useMemo, useState} from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import FeedbackSurvey from '@components/FeedbackSurvey'; import FixedFooter from '@components/FixedFooter'; @@ -12,21 +13,20 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as Report from '@userActions/Report'; -import type {CancellationType, FeedbackSurveyOptionID} from '@src/CONST'; +import * as Subscription from '@userActions/Subscription'; +import type {FeedbackSurveyOptionID} from '@src/CONST'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; function RequestEarlyCancellationPage() { const {translate} = useLocalize(); const styles = useThemeStyles(); - // TODO: replace this with NVP_PRIVATE_CANCELLATION_DETAILS.cancellationType - const [cancellationType, setCancellationType] = useState(); + const [cancellationDetails] = useOnyx(ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const handleSubmit = (cancellationReason: FeedbackSurveyOptionID, cancellationNote = '') => { - // TODO: call the CancelBillingSubscriptionNewDot API method - setCancellationType(CONST.CANCELLATION_TYPE.AUTOMATIC); + Subscription.cancelBillingSubscription(cancellationReason, cancellationNote); }; const acknowledgmentText = useMemo( @@ -107,7 +107,7 @@ function RequestEarlyCancellationPage() { let screenContent: React.ReactNode; - switch (cancellationType) { + switch (cancellationDetails?.cancellationType) { case CONST.CANCELLATION_TYPE.MANUAL: screenContent = manualCancellationContent; break; diff --git a/src/types/onyx/CancellationDetails.ts b/src/types/onyx/CancellationDetails.ts new file mode 100644 index 000000000000..9dcc62eaa84d --- /dev/null +++ b/src/types/onyx/CancellationDetails.ts @@ -0,0 +1,29 @@ +import type {CancellationType, FeedbackSurveyOptionID} from '@src/CONST'; +import type * as OnyxCommon from './OnyxCommon'; +import type PrivateSubscription from './PrivateSubscription'; + +/** Cancellation details model */ +type CancellationDetails = { + /** Cancellation date */ + cancellationDate: string; + + /** Cancellation reason */ + cancellationReason: FeedbackSurveyOptionID; + + /** Cancellation type (manual/automatic/none) */ + cancellationType: CancellationType; + + /** Additional note */ + note: string; + + /** Cancellation request date */ + requestDate: string; + + /** Canceled subscription object */ + subscription: PrivateSubscription; + + /** Cancellation errors */ + errors?: OnyxCommon.Errors; +}; + +export default CancellationDetails; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index e9f5143c975b..2538ae358894 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -6,6 +6,7 @@ import type Beta from './Beta'; import type BillingGraceEndPeriod from './BillingGraceEndPeriod'; import type BillingStatus from './BillingStatus'; import type BlockedFromConcierge from './BlockedFromConcierge'; +import type CancellationDetails from './CancellationDetails'; import type Card from './Card'; import type {CardList, IssueNewCard, WorkspaceCardsList} from './Card'; import type {CapturedLogs, Log} from './Console'; @@ -202,4 +203,5 @@ export type { BillingGraceEndPeriod, StripeCustomerID, BillingStatus, + CancellationDetails, }; From a956c5aaa3ac80c00df837da7392c509fdd5b9b7 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 10 Jul 2024 10:21:38 +0200 Subject: [PATCH 13/20] change onyx type to array --- src/ONYXKEYS.ts | 2 +- src/hooks/useCancellationType.ts | 21 +++++++++++++++ src/libs/actions/Subscription.ts | 26 ++++++++++++------- .../RequestEarlyCancellationPage/index.tsx | 7 +++-- src/types/onyx/CancellationDetails.ts | 12 ++++----- 5 files changed, 47 insertions(+), 21 deletions(-) create mode 100644 src/hooks/useCancellationType.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index e345bca783f6..e154d9d03fc5 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -808,7 +808,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_BILLING_FUND_ID]: number; [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number; [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number; - [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails; + [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[]; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/hooks/useCancellationType.ts b/src/hooks/useCancellationType.ts new file mode 100644 index 000000000000..006274cfc8d8 --- /dev/null +++ b/src/hooks/useCancellationType.ts @@ -0,0 +1,21 @@ +import {useOnyx} from 'react-native-onyx'; +import type {CancellationType} from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +function useCancellationType(): CancellationType | undefined { + const [cancellationDetails] = useOnyx(ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS); + + if (!cancellationDetails) { + return; + } + + if (cancellationDetails.length === 1) { + return cancellationDetails[0]?.cancellationType; + } + + const sorted = cancellationDetails?.sort((a, b) => new Date(b?.requestDate ?? 0).getDate() - new Date(a?.requestDate ?? 0).getDate()); + + return sorted[0].cancellationType; +} + +export default useCancellationType; diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index 864f92d99d0a..a5a520439d20 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -284,10 +284,12 @@ function cancelBillingSubscription(cancellationReason: FeedbackSurveyOptionID, c { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS, - value: { - cancellationReason, - errors: null, - }, + value: [ + { + cancellationReason, + errors: undefined, + }, + ], }, ]; @@ -295,9 +297,11 @@ function cancelBillingSubscription(cancellationReason: FeedbackSurveyOptionID, c { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS, - value: { - errors: null, - }, + value: [ + { + errors: undefined, + }, + ], }, ]; @@ -305,9 +309,11 @@ function cancelBillingSubscription(cancellationReason: FeedbackSurveyOptionID, c { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS, - value: { - cancellationType: null, - }, + value: [ + { + cancellationType: undefined, + }, + ], }, ]; diff --git a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx index 3324d579e75a..9361b97defc5 100644 --- a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx +++ b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx @@ -1,6 +1,5 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import FeedbackSurvey from '@components/FeedbackSurvey'; import FixedFooter from '@components/FixedFooter'; @@ -9,6 +8,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import useCancellationType from '@hooks/useCancellationType'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; @@ -16,14 +16,13 @@ import * as Report from '@userActions/Report'; import * as Subscription from '@userActions/Subscription'; import type {FeedbackSurveyOptionID} from '@src/CONST'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; function RequestEarlyCancellationPage() { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [cancellationDetails] = useOnyx(ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS); + const cancellationType = useCancellationType(); const handleSubmit = (cancellationReason: FeedbackSurveyOptionID, cancellationNote = '') => { Subscription.cancelBillingSubscription(cancellationReason, cancellationNote); @@ -107,7 +106,7 @@ function RequestEarlyCancellationPage() { let screenContent: React.ReactNode; - switch (cancellationDetails?.cancellationType) { + switch (cancellationType) { case CONST.CANCELLATION_TYPE.MANUAL: screenContent = manualCancellationContent; break; diff --git a/src/types/onyx/CancellationDetails.ts b/src/types/onyx/CancellationDetails.ts index 9dcc62eaa84d..18fb93160735 100644 --- a/src/types/onyx/CancellationDetails.ts +++ b/src/types/onyx/CancellationDetails.ts @@ -5,22 +5,22 @@ import type PrivateSubscription from './PrivateSubscription'; /** Cancellation details model */ type CancellationDetails = { /** Cancellation date */ - cancellationDate: string; + cancellationDate?: string; /** Cancellation reason */ - cancellationReason: FeedbackSurveyOptionID; + cancellationReason?: FeedbackSurveyOptionID; /** Cancellation type (manual/automatic/none) */ - cancellationType: CancellationType; + cancellationType?: CancellationType; /** Additional note */ - note: string; + note?: string; /** Cancellation request date */ - requestDate: string; + requestDate?: string; /** Canceled subscription object */ - subscription: PrivateSubscription; + subscription?: PrivateSubscription; /** Cancellation errors */ errors?: OnyxCommon.Errors; From ec7fd667d510726fb5e9976ddc115fa9dc4e1a84 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 10 Jul 2024 15:27:08 +0200 Subject: [PATCH 14/20] filter items by cancellationDate in useCancellationType hook --- src/hooks/useCancellationType.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/hooks/useCancellationType.ts b/src/hooks/useCancellationType.ts index 006274cfc8d8..a6a0b0d1cf76 100644 --- a/src/hooks/useCancellationType.ts +++ b/src/hooks/useCancellationType.ts @@ -5,17 +5,19 @@ import ONYXKEYS from '@src/ONYXKEYS'; function useCancellationType(): CancellationType | undefined { const [cancellationDetails] = useOnyx(ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS); - if (!cancellationDetails) { + const filteredCancellationDetails = cancellationDetails?.filter((item) => !item.cancellationDate); + + if (!filteredCancellationDetails) { return; } - if (cancellationDetails.length === 1) { - return cancellationDetails[0]?.cancellationType; + if (filteredCancellationDetails.length === 1) { + return filteredCancellationDetails[0]?.cancellationType; } - const sorted = cancellationDetails?.sort((a, b) => new Date(b?.requestDate ?? 0).getDate() - new Date(a?.requestDate ?? 0).getDate()); + const sorted = filteredCancellationDetails?.sort((a, b) => new Date(b?.requestDate ?? 0).getDate() - new Date(a?.requestDate ?? 0).getDate()); - return sorted[0].cancellationType; + return sorted[0]?.cancellationType; } export default useCancellationType; From 3378b782e83185780f4587de9924ad262f8403a8 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 16 Jul 2024 16:02:55 +0200 Subject: [PATCH 15/20] validate note field in FeedbackForm --- src/components/FeedbackSurvey.tsx | 7 +++++-- .../Subscription/RequestEarlyCancellationPage/index.tsx | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/FeedbackSurvey.tsx b/src/components/FeedbackSurvey.tsx index 8ad5dcd45bb4..41f2dd45a2fe 100644 --- a/src/components/FeedbackSurvey.tsx +++ b/src/components/FeedbackSurvey.tsx @@ -28,6 +28,9 @@ type FeedbackSurveyProps = { /** Optional text to render over the submit button */ footerText?: React.ReactNode; + + /** Indicates whether note field is required */ + isNoteRequired?: boolean; }; type Option = { @@ -42,7 +45,7 @@ const OPTIONS: Option[] = [ {key: CONST.FEEDBACK_SURVEY_OPTIONS.BUSINESS_CLOSING.ID, label: CONST.FEEDBACK_SURVEY_OPTIONS.BUSINESS_CLOSING.TRANSLATION_KEY}, ]; -function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerText}: FeedbackSurveyProps) { +function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerText, isNoteRequired}: FeedbackSurveyProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); @@ -58,7 +61,7 @@ function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerTe }; const handleSubmit = () => { - if (!reason) { + if (!reason || (isNoteRequired && !note.trim())) { setShouldShowReasonError(true); return; } diff --git a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx index 9361b97defc5..7b297767f224 100644 --- a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx +++ b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx @@ -99,6 +99,7 @@ function RequestEarlyCancellationPage() { onSubmit={handleSubmit} optionRowStyles={styles.flex1} footerText={{acknowledgmentText}} + isNoteRequired /> ), [acknowledgmentText, styles, translate], From 0f4081039f343eb99977ab77bbb40ea09e03eb3a Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 18 Jul 2024 14:37:16 +0200 Subject: [PATCH 16/20] fix the cancellation details logic --- src/hooks/useCancellationType.ts | 37 ++++++++++++++++++++++++-------- src/libs/actions/Subscription.ts | 29 +++++++++++++------------ 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/hooks/useCancellationType.ts b/src/hooks/useCancellationType.ts index a6a0b0d1cf76..43454b3504e8 100644 --- a/src/hooks/useCancellationType.ts +++ b/src/hooks/useCancellationType.ts @@ -1,23 +1,42 @@ +import {useEffect, useMemo, useRef, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import type {CancellationType} from '@src/CONST'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; function useCancellationType(): CancellationType | undefined { const [cancellationDetails] = useOnyx(ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS); - const filteredCancellationDetails = cancellationDetails?.filter((item) => !item.cancellationDate); + const [cancellationType, setCancellationType] = useState(); - if (!filteredCancellationDetails) { - return; - } + const previousCancellationDetails = useRef(cancellationDetails); - if (filteredCancellationDetails.length === 1) { - return filteredCancellationDetails[0]?.cancellationType; - } + const memoizedCancellationType = useMemo(() => { + const pendingManualCancellation = cancellationDetails?.filter((detail) => detail.cancellationType === CONST.CANCELLATION_TYPE.MANUAL).find((detail) => !detail.cancellationDate); - const sorted = filteredCancellationDetails?.sort((a, b) => new Date(b?.requestDate ?? 0).getDate() - new Date(a?.requestDate ?? 0).getDate()); + // There is a pending manual cancellation - return manual cancellation type + if (pendingManualCancellation) { + return CONST.CANCELLATION_TYPE.MANUAL; + } - return sorted[0]?.cancellationType; + // There are no new items in the cancellation details NVP + if (previousCancellationDetails.current?.length === cancellationDetails?.length) { + return; + } + + // There is a new item in the cancellation details NVP, it has to be an automatic cancellation, as pending manual cancellations are handled above + return CONST.CANCELLATION_TYPE.AUTOMATIC; + }, [cancellationDetails]); + + useEffect(() => { + if (!memoizedCancellationType) { + return; + } + + setCancellationType(memoizedCancellationType); + }, [memoizedCancellationType]); + + return cancellationType; } export default useCancellationType; diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index a5a520439d20..258977bfc358 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -6,8 +6,21 @@ import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import CONST from '@src/CONST'; import type {FeedbackSurveyOptionID, SubscriptionType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {CancellationDetails} from '@src/types/onyx'; import type {OnyxData} from '@src/types/onyx/Request'; +let cancellationDetails: CancellationDetails[] = []; +Onyx.connect({ + key: ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS, + callback: (value) => { + if (!value) { + return; + } + + cancellationDetails = value; + }, +}); + /** * Fetches data when the user opens the SubscriptionSettingsPage */ @@ -280,24 +293,12 @@ function clearOutstandingBalance() { } function cancelBillingSubscription(cancellationReason: FeedbackSurveyOptionID, cancellationNote: string) { - const optimisticData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS, - value: [ - { - cancellationReason, - errors: undefined, - }, - ], - }, - ]; - const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS, value: [ + ...cancellationDetails, { errors: undefined, }, @@ -310,6 +311,7 @@ function cancelBillingSubscription(cancellationReason: FeedbackSurveyOptionID, c onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS, value: [ + ...cancellationDetails, { cancellationType: undefined, }, @@ -323,7 +325,6 @@ function cancelBillingSubscription(cancellationReason: FeedbackSurveyOptionID, c }; API.write(WRITE_COMMANDS.CANCEL_BILLING_SUBSCRIPTION, parameters, { - optimisticData, successData, failureData, }); From f9b9258f6531a1a47f0d5a292311aba8c2a207c4 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 19 Jul 2024 14:35:29 +0200 Subject: [PATCH 17/20] apply suggested changes --- src/components/FeedbackSurvey.tsx | 2 +- src/hooks/useCancellationType.ts | 1 + .../RequestEarlyCancellationPage/index.tsx | 25 ++++++++----------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/components/FeedbackSurvey.tsx b/src/components/FeedbackSurvey.tsx index 41f2dd45a2fe..2925e810c497 100644 --- a/src/components/FeedbackSurvey.tsx +++ b/src/components/FeedbackSurvey.tsx @@ -102,7 +102,7 @@ function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerTe message={translate('common.error.pleaseCompleteForm')} buttonText={translate('common.submit')} enabledWhenOffline - containerStyles={styles.mt2} + containerStyles={styles.mt3} /> diff --git a/src/hooks/useCancellationType.ts b/src/hooks/useCancellationType.ts index 43454b3504e8..bc34f5feea6f 100644 --- a/src/hooks/useCancellationType.ts +++ b/src/hooks/useCancellationType.ts @@ -9,6 +9,7 @@ function useCancellationType(): CancellationType | undefined { const [cancellationType, setCancellationType] = useState(); + // Store initial cancellation details array in a ref for comparison const previousCancellationDetails = useRef(cancellationDetails); const memoizedCancellationType = useMemo(() => { diff --git a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx index 7b297767f224..5deec421a76a 100644 --- a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx +++ b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx @@ -1,3 +1,4 @@ +import type {ReactNode} from 'react'; import React, {useMemo} from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; @@ -14,7 +15,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as Report from '@userActions/Report'; import * as Subscription from '@userActions/Subscription'; -import type {FeedbackSurveyOptionID} from '@src/CONST'; +import type {CancellationType, FeedbackSurveyOptionID} from '@src/CONST'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -28,7 +29,7 @@ function RequestEarlyCancellationPage() { Subscription.cancelBillingSubscription(cancellationReason, cancellationNote); }; - const acknowledgmentText = useMemo( + const acknowledgementText = useMemo( () => ( {translate('subscription.requestEarlyCancellation.acknowledgement.part1')} @@ -98,25 +99,19 @@ function RequestEarlyCancellationPage() { description={translate('subscription.requestEarlyCancellation.subtitle')} onSubmit={handleSubmit} optionRowStyles={styles.flex1} - footerText={{acknowledgmentText}} + footerText={{acknowledgementText}} isNoteRequired /> ), - [acknowledgmentText, styles, translate], + [acknowledgementText, styles, translate], ); - let screenContent: React.ReactNode; + const contentMap: Partial> = { + [CONST.CANCELLATION_TYPE.MANUAL]: manualCancellationContent, + [CONST.CANCELLATION_TYPE.AUTOMATIC]: automaticCancellationContent, + }; - switch (cancellationType) { - case CONST.CANCELLATION_TYPE.MANUAL: - screenContent = manualCancellationContent; - break; - case CONST.CANCELLATION_TYPE.AUTOMATIC: - screenContent = automaticCancellationContent; - break; - default: - screenContent = surveyContent; - } + const screenContent = cancellationType ? contentMap[cancellationType] : surveyContent; return ( Date: Tue, 23 Jul 2024 11:47:27 +0200 Subject: [PATCH 18/20] make cancellationReason a mandatory param --- src/libs/actions/Subscription.ts | 9 ++------- src/types/onyx/CancellationDetails.ts | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index 258977bfc358..bf23a43dc0a9 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -300,7 +300,7 @@ function cancelBillingSubscription(cancellationReason: FeedbackSurveyOptionID, c value: [ ...cancellationDetails, { - errors: undefined, + cancellationReason, }, ], }, @@ -310,12 +310,7 @@ function cancelBillingSubscription(cancellationReason: FeedbackSurveyOptionID, c { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS, - value: [ - ...cancellationDetails, - { - cancellationType: undefined, - }, - ], + value: [...cancellationDetails], }, ]; diff --git a/src/types/onyx/CancellationDetails.ts b/src/types/onyx/CancellationDetails.ts index 18fb93160735..47b172ac4228 100644 --- a/src/types/onyx/CancellationDetails.ts +++ b/src/types/onyx/CancellationDetails.ts @@ -8,7 +8,7 @@ type CancellationDetails = { cancellationDate?: string; /** Cancellation reason */ - cancellationReason?: FeedbackSurveyOptionID; + cancellationReason: FeedbackSurveyOptionID; /** Cancellation type (manual/automatic/none) */ cancellationType?: CancellationType; From 43f58f7f7d5f6ec4ee8b21e52c1ec7f1c81ba233 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 24 Jul 2024 14:18:38 +0200 Subject: [PATCH 19/20] remove success and failure data from cancelBillingSubscription --- src/libs/actions/Subscription.ts | 39 +-------------------------- src/types/onyx/CancellationDetails.ts | 2 +- 2 files changed, 2 insertions(+), 39 deletions(-) diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index bf23a43dc0a9..aaec38dbf5f8 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -6,21 +6,8 @@ import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import CONST from '@src/CONST'; import type {FeedbackSurveyOptionID, SubscriptionType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {CancellationDetails} from '@src/types/onyx'; import type {OnyxData} from '@src/types/onyx/Request'; -let cancellationDetails: CancellationDetails[] = []; -Onyx.connect({ - key: ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS, - callback: (value) => { - if (!value) { - return; - } - - cancellationDetails = value; - }, -}); - /** * Fetches data when the user opens the SubscriptionSettingsPage */ @@ -293,36 +280,12 @@ function clearOutstandingBalance() { } function cancelBillingSubscription(cancellationReason: FeedbackSurveyOptionID, cancellationNote: string) { - const successData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS, - value: [ - ...cancellationDetails, - { - cancellationReason, - }, - ], - }, - ]; - - const failureData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS, - value: [...cancellationDetails], - }, - ]; - const parameters: CancelBillingSubscriptionParams = { cancellationReason, cancellationNote, }; - API.write(WRITE_COMMANDS.CANCEL_BILLING_SUBSCRIPTION, parameters, { - successData, - failureData, - }); + API.write(WRITE_COMMANDS.CANCEL_BILLING_SUBSCRIPTION, parameters); } export { diff --git a/src/types/onyx/CancellationDetails.ts b/src/types/onyx/CancellationDetails.ts index 47b172ac4228..fa0f8f51a8a5 100644 --- a/src/types/onyx/CancellationDetails.ts +++ b/src/types/onyx/CancellationDetails.ts @@ -11,7 +11,7 @@ type CancellationDetails = { cancellationReason: FeedbackSurveyOptionID; /** Cancellation type (manual/automatic/none) */ - cancellationType?: CancellationType; + cancellationType: CancellationType; /** Additional note */ note?: string; From 9339f3129c421632cea4b887c8565bc673b4fd4c Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 24 Jul 2024 15:27:07 +0200 Subject: [PATCH 20/20] add a loading indicator to early cancellation form --- src/components/FeedbackSurvey.tsx | 6 +++++- .../Subscription/RequestEarlyCancellationPage/index.tsx | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/FeedbackSurvey.tsx b/src/components/FeedbackSurvey.tsx index 2925e810c497..db5db3ee447d 100644 --- a/src/components/FeedbackSurvey.tsx +++ b/src/components/FeedbackSurvey.tsx @@ -31,6 +31,9 @@ type FeedbackSurveyProps = { /** Indicates whether note field is required */ isNoteRequired?: boolean; + + /** Indicates whether a loading indicator should be shown */ + isLoading?: boolean; }; type Option = { @@ -45,7 +48,7 @@ const OPTIONS: Option[] = [ {key: CONST.FEEDBACK_SURVEY_OPTIONS.BUSINESS_CLOSING.ID, label: CONST.FEEDBACK_SURVEY_OPTIONS.BUSINESS_CLOSING.TRANSLATION_KEY}, ]; -function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerText, isNoteRequired}: FeedbackSurveyProps) { +function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerText, isNoteRequired, isLoading}: FeedbackSurveyProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); @@ -103,6 +106,7 @@ function FeedbackSurvey({title, description, onSubmit, optionRowStyles, footerTe buttonText={translate('common.submit')} enabledWhenOffline containerStyles={styles.mt3} + isLoading={isLoading} /> diff --git a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx index 5deec421a76a..4e691a57a48d 100644 --- a/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx +++ b/src/pages/settings/Subscription/RequestEarlyCancellationPage/index.tsx @@ -1,5 +1,5 @@ import type {ReactNode} from 'react'; -import React, {useMemo} from 'react'; +import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; import FeedbackSurvey from '@components/FeedbackSurvey'; @@ -23,9 +23,12 @@ function RequestEarlyCancellationPage() { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [isLoading, setIsLoading] = useState(false); + const cancellationType = useCancellationType(); const handleSubmit = (cancellationReason: FeedbackSurveyOptionID, cancellationNote = '') => { + setIsLoading(true); Subscription.cancelBillingSubscription(cancellationReason, cancellationNote); }; @@ -101,9 +104,10 @@ function RequestEarlyCancellationPage() { optionRowStyles={styles.flex1} footerText={{acknowledgementText}} isNoteRequired + isLoading={isLoading} /> ), - [acknowledgementText, styles, translate], + [acknowledgementText, isLoading, styles.flex1, styles.mb2, styles.mt4, translate], ); const contentMap: Partial> = {