From 326f2e67703efb00751242d855b742e4c2418e35 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Sun, 8 Mar 2026 00:56:50 +0000 Subject: [PATCH 01/18] Add support for Personal Karma updates and related API parameters - Introduced ONYXKEYS for managing Personal Karma donation states. - Added new API command and parameters for updating Personal Karma. - Updated Subscription actions to include a function for updating Personal Karma. - Added a help URL for Personal and Corporate Karma in the constants. --- src/CONST/index.ts | 1 + src/ONYXKEYS.ts | 4 +++ .../parameters/UpdatePersonalKarmaParams.ts | 5 +++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 ++ src/libs/actions/Subscription.ts | 36 ++++++++++++++++++- 6 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 src/libs/API/parameters/UpdatePersonalKarmaParams.ts diff --git a/src/CONST/index.ts b/src/CONST/index.ts index e8d303b1b8e1..70909f9603da 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1106,6 +1106,7 @@ const CONST = { BULK_UPLOAD_HELP_URL: 'https://help.expensify.com/articles/new-expensify/reports-and-expenses/Create-an-Expense#option-4-bulk-upload-receipts-desktop-only', ENCRYPTION_AND_SECURITY_HELP_URL: 'https://help.expensify.com/articles/new-expensify/settings/Encryption-and-Data-Security', PLAN_TYPES_AND_PRICING_HELP_URL: 'https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing', + PERSONAL_AND_CORPORATE_KARMA_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/expensify-billing/Personal-and-Corporate-Karma', COLLECT_UPGRADE_HELP_URL: 'https://help.expensify.com/Hidden/collect-upgrade', MERGE_ACCOUNT_HELP_URL: 'https://help.expensify.com/articles/new-expensify/settings/Merge-Accounts', CONNECT_A_BUSINESS_BANK_ACCOUNT_HELP_URL: 'https://help.expensify.com/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d812524bc692..63390bf0991f 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -225,6 +225,9 @@ const ONYXKEYS = { /** Store the state of the subscription */ NVP_PRIVATE_SUBSCRIPTION: 'nvp_private_subscription', + /** Store the state of Personal Karma donations */ + NVP_PERSONAL_OFFSETS: 'nvp_expensify_enablePersonalOffsets', + /** Store the state of the private tax-exempt */ NVP_PRIVATE_TAX_EXEMPT: 'nvp_private_taxExempt', @@ -1285,6 +1288,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS]: OnyxTypes.DismissedReferralBanners; [ONYXKEYS.NVP_HAS_SEEN_TRACK_TRAINING]: boolean; [ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION]: OnyxTypes.PrivateSubscription; + [ONYXKEYS.NVP_PERSONAL_OFFSETS]: boolean; [ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID]: OnyxTypes.StripeCustomerID; [ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING]: number; [ONYXKEYS.NVP_PRIVATE_BILLING_STATUS]: OnyxTypes.BillingStatus; diff --git a/src/libs/API/parameters/UpdatePersonalKarmaParams.ts b/src/libs/API/parameters/UpdatePersonalKarmaParams.ts new file mode 100644 index 000000000000..eb512096b9d6 --- /dev/null +++ b/src/libs/API/parameters/UpdatePersonalKarmaParams.ts @@ -0,0 +1,5 @@ +type UpdatePersonalKarmaParams = { + enabled: boolean; +}; + +export default UpdatePersonalKarmaParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index e3f8f62f3b24..d590dd071422 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -290,6 +290,7 @@ export type {default as UpdateSubscriptionTypeParams} from './UpdateSubscription export type {default as SignUpUserParams} from './SignUpUserParams'; export type {default as UpdateSubscriptionAutoRenewParams} from './UpdateSubscriptionAutoRenewParams'; export type {default as UpdateSubscriptionAddNewUsersAutomaticallyParams} from './UpdateSubscriptionAddNewUsersAutomaticallyParams'; +export type {default as UpdatePersonalKarmaParams} from './UpdatePersonalKarmaParams'; export type {default as GenerateSpotnanaTokenParams} from './GenerateSpotnanaTokenParams'; export type {default as UpdateSubscriptionSizeParams} from './UpdateSubscriptionSizeParams'; export type {default as SetPromoCodeParams} from './SetPromoCodeParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 97dfdee78c76..e610d2360b87 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -364,6 +364,7 @@ const WRITE_COMMANDS = { SIGN_UP_USER: 'SignUpUser', UPDATE_SUBSCRIPTION_AUTO_RENEW: 'UpdateSubscriptionAutoRenew', UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY: 'UpdateSubscriptionAddNewUsersAutomatically', + UPDATE_PERSONAL_KARMA: 'UpdatePersonalKarma', UPDATE_SUBSCRIPTION_SIZE: 'UpdateSubscriptionSize', REPORT_EXPORT: 'Report_Export', MARK_AS_EXPORTED: 'MarkAsExported', @@ -936,6 +937,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SIGN_UP_USER]: Parameters.SignUpUserParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_AUTO_RENEW]: Parameters.UpdateSubscriptionAutoRenewParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY]: Parameters.UpdateSubscriptionAddNewUsersAutomaticallyParams; + [WRITE_COMMANDS.UPDATE_PERSONAL_KARMA]: Parameters.UpdatePersonalKarmaParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_SIZE]: Parameters.UpdateSubscriptionSizeParams; [WRITE_COMMANDS.SET_PROMO_CODE]: Parameters.SetPromoCodeParams; [WRITE_COMMANDS.REQUEST_TAX_EXEMPTION]: null; diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index e838ea9d1481..2ac72844624d 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -1,7 +1,13 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import type {CancelBillingSubscriptionParams, UpdateSubscriptionAddNewUsersAutomaticallyParams, UpdateSubscriptionAutoRenewParams, UpdateSubscriptionTypeParams} from '@libs/API/parameters'; +import type { + CancelBillingSubscriptionParams, + UpdatePersonalKarmaParams, + UpdateSubscriptionAddNewUsersAutomaticallyParams, + UpdateSubscriptionAutoRenewParams, + UpdateSubscriptionTypeParams, +} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import CONST from '@src/CONST'; @@ -175,6 +181,33 @@ function updateSubscriptionAddNewUsersAutomatically(addNewUsersAutomatically: bo }); } +function updatePersonalKarma(enabled: boolean) { + const optimisticData: Array> = [ + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.NVP_PERSONAL_OFFSETS, + value: enabled, + }, + ]; + + const failureData: Array> = [ + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.NVP_PERSONAL_OFFSETS, + value: !enabled, + }, + ]; + + const parameters: UpdatePersonalKarmaParams = { + enabled, + }; + + API.write(WRITE_COMMANDS.UPDATE_PERSONAL_KARMA, parameters, { + optimisticData, + failureData, + }); +} + function updateSubscriptionSize(newSubscriptionSize: number, currentSubscriptionSize: number) { const onyxData: OnyxData = { optimisticData: [ @@ -356,6 +389,7 @@ export { openSubscriptionPage, updateSubscriptionAutoRenew, updateSubscriptionAddNewUsersAutomatically, + updatePersonalKarma, updateSubscriptionSize, clearUpdateSubscriptionSizeError, updateSubscriptionType, From 490e101255a11523f54ef628cb38b4deb01c75ed Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Sun, 8 Mar 2026 01:24:28 +0000 Subject: [PATCH 02/18] Add BillingCardDetails component and integrate into CardSection - Created a new BillingCardDetails component to encapsulate billing card information display. - Replaced inline billing card rendering in CardSection with the new BillingCardDetails component for improved code organization and readability. - Removed unused imports and optimized existing code in CardSection. --- src/components/BillingCardDetails.tsx | 57 +++++++++++++++++++ .../Subscription/CardSection/CardSection.tsx | 33 ++--------- 2 files changed, 63 insertions(+), 27 deletions(-) create mode 100644 src/components/BillingCardDetails.tsx diff --git a/src/components/BillingCardDetails.tsx b/src/components/BillingCardDetails.tsx new file mode 100644 index 000000000000..a3cfad16eaed --- /dev/null +++ b/src/components/BillingCardDetails.tsx @@ -0,0 +1,57 @@ +import type {ReactNode} from 'react'; +import React, {useMemo} from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import Text from '@components/Text'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import DateUtils from '@libs/DateUtils'; +import {getPaymentMethodDescription} from '@libs/PaymentUtils'; +import type Fund from '@src/types/onyx/Fund'; + +type BillingCardDetailsProps = { + /** The billing card data */ + card?: Fund; + + /** Optional right side content (e.g. action menu) */ + rightComponent?: ReactNode; + + /** Optional wrapper styles */ + wrapperStyle?: StyleProp; +}; + +function BillingCardDetails({card, rightComponent, wrapperStyle}: BillingCardDetailsProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['CreditCard']); + + const cardMonth = useMemo(() => DateUtils.getMonthNames()[(card?.accountData?.cardMonth ?? 1) - 1], [card?.accountData?.cardMonth]); + + if (!card?.accountData) { + return null; + } + + return ( + + + + {getPaymentMethodDescription(card.accountType, card.accountData, translate)} + + {translate('subscription.cardSection.cardInfo', card.accountData.addressName ?? '', `${cardMonth} ${card.accountData.cardYear ?? ''}`, card.accountData.currency ?? '')} + + + {rightComponent} + + ); +} + +export default BillingCardDetails; diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 850bf02244c7..794c567b3229 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; +import BillingCardDetails from '@components/BillingCardDetails'; import ConfirmModal from '@components/ConfirmModal'; -import Icon from '@components/Icon'; import MenuItem from '@components/MenuItem'; import RenderHTML from '@components/RenderHTML'; import Section from '@components/Section'; @@ -13,12 +13,9 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePrivateSubscription from '@hooks/usePrivateSubscription'; import useSubscriptionPlan from '@hooks/useSubscriptionPlan'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {requestRefund as requestRefundByUser} from '@libs/actions/User'; -import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getPaymentMethodDescription} from '@libs/PaymentUtils'; import {buildQueryStringFromFilterFormValues} from '@libs/SearchQueryUtils'; import {hasCardAuthenticatedError, hasUserFreeTrialEnded, isUserOnFreeTrial, shouldShowDiscountBanner, shouldShowPreTrialBillingBanner} from '@libs/SubscriptionUtils'; import {verifySetupIntent} from '@userActions/PaymentMethods'; @@ -44,8 +41,7 @@ function CardSection() { const [isRequestRefundModalVisible, setIsRequestRefundModalVisible] = useState(false); const {translate} = useLocalize(); const styles = useThemeStyles(); - const theme = useTheme(); - const expensifyIcons = useMemoizedLazyExpensifyIcons(['History', 'Bill', 'CreditCard']); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['History', 'Bill']); const illustrations = useMemoizedLazyIllustrations(['CreditCardEyes']); const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); const privateSubscription = usePrivateSubscription(); @@ -62,7 +58,6 @@ function CardSection() { const [subscriptionRetryBillingStatusFailed] = useOnyx(ONYXKEYS.SUBSCRIPTION_RETRY_BILLING_STATUS_FAILED, {canBeMissing: true}); const {isOffline} = useNetwork(); const defaultCard = useMemo(() => Object.values(fundList ?? {}).find((card) => card.accountData?.additionalData?.isBillingCard), [fundList]); - const cardMonth = useMemo(() => DateUtils.getMonthNames()[(defaultCard?.accountData?.cardMonth ?? 1) - 1], [defaultCard?.accountData?.cardMonth]); const hasFailedLastBilling = useMemo( () => purchaseList?.[0]?.message.billingType === CONST.BILLING.TYPE_STRIPE_FAILED_AUTHENTICATION || purchaseList?.[0]?.message.billingType === CONST.BILLING.TYPE_FAILED_2018, [purchaseList], @@ -204,26 +199,10 @@ function CardSection() { > {!isEmptyObject(defaultCard?.accountData) && ( - - - - {getPaymentMethodDescription(defaultCard?.accountType, defaultCard?.accountData, translate)} - - {translate( - 'subscription.cardSection.cardInfo', - defaultCard?.accountData?.addressName ?? '', - `${cardMonth} ${defaultCard?.accountData?.cardYear}`, - defaultCard?.accountData?.currency ?? '', - )} - - - - + } + /> )} From 5cb185c1187b8fc6fd7e012f76ce482c1922d71b Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Sun, 8 Mar 2026 01:27:08 +0000 Subject: [PATCH 03/18] Add Personal Karma translations for multiple languages - Added new translations for the Personal Karma feature in German, English, Spanish, French, Italian, Japanese, Dutch, Polish, Brazilian Portuguese, and Simplified Chinese. - Each translation includes title, description, prompts for adding a payment card, stopping donations, and the title for the donation card. --- src/languages/de.ts | 7 +++++++ src/languages/en.ts | 7 +++++++ src/languages/es.ts | 7 +++++++ src/languages/fr.ts | 7 +++++++ src/languages/it.ts | 7 +++++++ src/languages/ja.ts | 7 +++++++ src/languages/nl.ts | 7 +++++++ src/languages/pl.ts | 7 +++++++ src/languages/pt-BR.ts | 7 +++++++ src/languages/zh-hans.ts | 7 +++++++ 10 files changed, 70 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index b4f140303108..5a7d85223e21 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7502,6 +7502,13 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und 'Schließ dich Expensify.org an, um Ungerechtigkeit auf der ganzen Welt zu beseitigen. Die aktuelle „Teachers Unite“-Kampagne unterstützt Lehrkräfte überall, indem sie die Kosten für grundlegende Schulmaterialien teilt.', iKnowATeacher: 'Ich kenne eine Lehrkraft', iAmATeacher: 'Ich bin Lehrer', + personalKarma: { + title: 'Persönliches Karma', + description: 'Spende 1 $ an Expensify.org für jeweils 500 $, die du pro Monat ausgibst', + addPaymentCardPrompt: 'Füge eine Zahlungskarte hinzu, um persönliche Karma-Spenden zu aktivieren.', + stopDonationsPrompt: 'Bist du sicher? Deine monatliche Karma-Spende bewirkt viel.', + donationCardTitle: 'Für Spenden verwendete Karte', + }, getInTouch: 'Ausgezeichnet! Bitte teile ihre Kontaktdaten, damit wir sie erreichen können.', introSchoolPrincipal: 'Einführung für Ihre Schulleitung', schoolPrincipalVerifyExpense: diff --git a/src/languages/en.ts b/src/languages/en.ts index b9813b075f67..872eb77b1a77 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7451,6 +7451,13 @@ const translations = { 'Join Expensify.org in eliminating injustice around the world. The current "Teachers Unite" campaign supports educators everywhere by splitting the costs of essential school supplies.', iKnowATeacher: 'I know a teacher', iAmATeacher: 'I am a teacher', + personalKarma: { + title: 'Personal Karma', + description: 'Donate $1 to Expensify.org for every $500 you spend each month', + addPaymentCardPrompt: 'Add a payment card to enable Personal Karma donations.', + stopDonationsPrompt: 'Are you sure? Your monthly Karma donation makes a huge impact.', + donationCardTitle: 'Card used for donations', + }, getInTouch: 'Excellent! Please share their information so we can get in touch with them.', introSchoolPrincipal: 'Intro to your school principal', schoolPrincipalVerifyExpense: diff --git a/src/languages/es.ts b/src/languages/es.ts index d36023e9b4f8..2f2f55661efb 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7767,6 +7767,13 @@ ${amount} para ${merchant} - ${date}`, joinExpensifyOrg: 'Únete a Expensify.org para eliminar la injusticia en todo el mundo y ayuda a los profesores a dividir sus gastos para las aulas más necesitadas.', iKnowATeacher: 'Yo conozco a un profesor', iAmATeacher: 'Soy profesor', + personalKarma: { + title: 'Karma personal', + description: 'Dona $1 a Expensify.org por cada $500 que gastes cada mes', + addPaymentCardPrompt: 'Agrega una tarjeta de pago para habilitar las donaciones de Karma personal.', + stopDonationsPrompt: '¿Estás seguro? Tu donación mensual de Karma tiene un gran impacto.', + donationCardTitle: 'Tarjeta usada para donaciones', + }, getInTouch: '¡Excelente! Por favor, comparte tu información para que podamos ponernos en contacto con ellos.', introSchoolPrincipal: 'Introducción al director del colegio', schoolPrincipalVerifyExpense: diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 809aa73933c7..e5674920e1bc 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7523,6 +7523,13 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip 'Rejoignez Expensify.org pour éliminer l’injustice dans le monde entier. La campagne actuelle « Teachers Unite » soutient les enseignants partout en partageant le coût des fournitures scolaires essentielles.', iKnowATeacher: 'Je connais un enseignant', iAmATeacher: 'Je suis enseignant', + personalKarma: { + title: 'Karma personnel', + description: 'Faites un don de 1 $ à Expensify.org pour chaque tranche de 500 $ dépensée chaque mois', + addPaymentCardPrompt: 'Ajoutez une carte de paiement pour activer les dons de Karma personnel.', + stopDonationsPrompt: 'Êtes-vous sûr ? Votre don mensuel Karma a un impact considérable.', + donationCardTitle: 'Carte utilisée pour les dons', + }, getInTouch: 'Excellent ! Veuillez partager leurs coordonnées afin que nous puissions les contacter.', introSchoolPrincipal: 'Présentation de la direction de votre école', schoolPrincipalVerifyExpense: diff --git a/src/languages/it.ts b/src/languages/it.ts index ebba8e0b9376..7d5b38727b08 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7488,6 +7488,13 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo 'Unisciti a Expensify.org per eliminare le ingiustizie in tutto il mondo. L’attuale campagna “Teachers Unite” supporta gli insegnanti ovunque, dividendo i costi delle forniture scolastiche essenziali.', iKnowATeacher: 'Conosco un insegnante', iAmATeacher: 'Sono un insegnante', + personalKarma: { + title: 'Karma personale', + description: 'Dona 1 $ a Expensify.org per ogni 500 $ che spendi ogni mese', + addPaymentCardPrompt: 'Aggiungi una carta di pagamento per abilitare le donazioni di Karma personale.', + stopDonationsPrompt: 'Sei sicuro? La tua donazione mensile Karma ha un grande impatto.', + donationCardTitle: 'Carta usata per le donazioni', + }, getInTouch: 'Eccellente! Condividi le loro informazioni così possiamo metterci in contatto con loro.', introSchoolPrincipal: 'Introduzione al dirigente scolastico', schoolPrincipalVerifyExpense: diff --git a/src/languages/ja.ts b/src/languages/ja.ts index f9942a74e38f..0927c0f82967 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7414,6 +7414,13 @@ ${reportName} 'Expensify.org に参加して、世界中の不公正の解消に取り組みましょう。現在実施中の「Teachers Unite」キャンペーンでは、必需の学用品の費用を分担することで、すべての教育者を支援しています。', iKnowATeacher: '私は先生を知っています', iAmATeacher: '私は教師です', + personalKarma: { + title: 'パーソナルカルマ', + description: '毎月の支出500ドルごとに1ドルをExpensify.orgに寄付します', + addPaymentCardPrompt: 'パーソナルカルマの寄付を有効にするには、支払いカードを追加してください。', + stopDonationsPrompt: '本当によろしいですか?毎月のKarma寄付は大きな影響をもたらします。', + donationCardTitle: '寄付に使用するカード', + }, getInTouch: '素晴らしいです!その方の情報を共有していただければ、こちらからご連絡いたします。', introSchoolPrincipal: '学校校長への紹介', schoolPrincipalVerifyExpense: 'Expensify.org は、低所得世帯の生徒がより良い学習体験を得られるよう、必要な学用品の費用を分担します。あなたの経費は、校長により確認されます。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 95d7cd4f6b03..a54093501121 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7467,6 +7467,13 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar 'Sluit je aan bij Expensify.org om onrecht overal ter wereld uit te bannen. De huidige campagne “Teachers Unite” ondersteunt onderwijzers overal door de kosten van essentiële schoolbenodigdheden te delen.', iKnowATeacher: 'Ik ken een leraar', iAmATeacher: 'Ik ben een leraar', + personalKarma: { + title: 'Persoonlijke karma', + description: 'Doneer $1 aan Expensify.org voor elke $500 die je elke maand uitgeeft', + addPaymentCardPrompt: 'Voeg een betaalkaart toe om persoonlijke karma-donaties in te schakelen.', + stopDonationsPrompt: 'Weet je het zeker? Je maandelijkse Karma-donatie heeft een grote impact.', + donationCardTitle: 'Kaart gebruikt voor donaties', + }, getInTouch: 'Uitstekend! Deel hun gegevens zodat we contact met hen kunnen opnemen.', introSchoolPrincipal: 'Introductie bij je schooldirecteur', schoolPrincipalVerifyExpense: diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 59a3b8858ba3..6f7131e48482 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7454,6 +7454,13 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i 'Dołącz do Expensify.org w eliminowaniu niesprawiedliwości na całym świecie. Obecna kampania „Teachers Unite” wspiera nauczycieli wszędzie, dzieląc koszty niezbędnych przyborów szkolnych.', iKnowATeacher: 'Znam nauczyciela', iAmATeacher: 'Jestem nauczycielem', + personalKarma: { + title: 'Karma osobista', + description: 'Przekaż 1 USD na Expensify.org za każde 500 USD wydane w miesiącu', + addPaymentCardPrompt: 'Dodaj kartę płatniczą, aby włączyć darowizny w ramach Osobistej Karmy.', + stopDonationsPrompt: 'Czy na pewno? Twoja comiesięczna darowizna Karma ma ogromny wpływ.', + donationCardTitle: 'Karta używana do darowizn', + }, getInTouch: 'Świetnie! Udostępnij ich dane kontaktowe, abyśmy mogli się z nimi skontaktować.', introSchoolPrincipal: 'Wprowadzenie dla dyrektora szkoły', schoolPrincipalVerifyExpense: diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index cb056498d33f..5756ac1bff7a 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7459,6 +7459,13 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e 'Junte-se à Expensify.org para eliminar as injustiças ao redor do mundo. A atual campanha “Teachers Unite” apoia educadores em todos os lugares ao dividir os custos de materiais escolares essenciais.', iKnowATeacher: 'Eu conheço um professor', iAmATeacher: 'Sou professor(a)', + personalKarma: { + title: 'Karma pessoal', + description: 'Doe US$ 1 para o Expensify.org a cada US$ 500 que você gastar por mês', + addPaymentCardPrompt: 'Adicione um cartão de pagamento para ativar as doações do Karma pessoal.', + stopDonationsPrompt: 'Tem certeza? Sua doação mensal de Karma causa um grande impacto.', + donationCardTitle: 'Cartão usado para doações', + }, getInTouch: 'Excelente! Compartilhe as informações deles para que possamos entrar em contato.', introSchoolPrincipal: 'Apresentação ao diretor da sua escola', schoolPrincipalVerifyExpense: diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index de176b57f910..39da42ad818b 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7296,6 +7296,13 @@ ${reportName} joinExpensifyOrg: '加入 Expensify.org,一起消除世界各地的不公现象。当前的“教师联合”活动通过分担基本学习用品的费用来支持全世界的教育工作者。', iKnowATeacher: '我认识一位老师', iAmATeacher: '我是老师', + personalKarma: { + title: '个人 Karma', + description: '您每月每消费 500 美元,就向 Expensify.org 捐赠 1 美元', + addPaymentCardPrompt: '请添加支付卡以启用个人 Karma 捐赠。', + stopDonationsPrompt: '你确定吗?你每月的 Karma 捐赠会带来巨大影响。', + donationCardTitle: '用于捐赠的卡片', + }, getInTouch: '太好了!请分享他们的联系方式,以便我们与他们取得联系。', introSchoolPrincipal: '向你们学校校长的介绍', schoolPrincipalVerifyExpense: 'Expensify.org 会分摊基本学习用品的费用,让来自低收入家庭的学生能够拥有更好的学习体验。我们会请你的校长核实你的报销。', From faefa73ecc50d24a0f0c65d73cf4ccd333945f01 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Sun, 8 Mar 2026 01:59:09 +0000 Subject: [PATCH 04/18] Add SectionSubtitleHTML component and refactor subtitle rendering - Introduced a new SectionSubtitleHTML component to handle HTML subtitles with optional muted styling and link press handling. - Replaced instances of RenderHTML in various pages (DomainSamlPage, SecuritySettingsPage, TroubleshootPage, WorkspaceReportsPage, IndividualExpenseRulesSection) with SectionSubtitleHTML for improved consistency and maintainability. - Updated related styles and props to accommodate the new component. --- src/components/SectionSubtitleHTML.tsx | 37 +++++++++++++++++++ src/pages/domain/DomainSamlPage.tsx | 11 +++--- .../Security/SecuritySettingsPage.tsx | 17 +++------ .../Troubleshoot/TroubleshootPage.tsx | 8 +--- .../reports/WorkspaceReportsPage.tsx | 9 +++-- .../rules/IndividualExpenseRulesSection.tsx | 13 ++----- 6 files changed, 58 insertions(+), 37 deletions(-) create mode 100644 src/components/SectionSubtitleHTML.tsx diff --git a/src/components/SectionSubtitleHTML.tsx b/src/components/SectionSubtitleHTML.tsx new file mode 100644 index 000000000000..07179be3e414 --- /dev/null +++ b/src/components/SectionSubtitleHTML.tsx @@ -0,0 +1,37 @@ +import type {ComponentProps} from 'react'; +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import RenderHTML from '@components/RenderHTML'; +import useThemeStyles from '@hooks/useThemeStyles'; + +type SectionSubtitleHTMLProps = { + /** Subtitle HTML content */ + html: string; + + /** Whether the subtitle text should be muted */ + subtitleMuted?: boolean; + + /** Optional link press handler */ + onLinkPress?: ComponentProps['onLinkPress']; + + /** Optional wrapper style */ + wrapperStyle?: StyleProp; +}; + +function SectionSubtitleHTML({html, subtitleMuted = false, onLinkPress, wrapperStyle}: SectionSubtitleHTMLProps) { + const styles = useThemeStyles(); + const shouldWrapWithMutedText = subtitleMuted && !/<\s*muted-text(?:-[a-z]+)?(?:\s|>)/.test(html); + const subtitleHTML = shouldWrapWithMutedText ? `${html}` : html; + + return ( + + + + ); +} + +export default SectionSubtitleHTML; diff --git a/src/pages/domain/DomainSamlPage.tsx b/src/pages/domain/DomainSamlPage.tsx index 7da6fa51f2e7..d1fa13a81cb9 100644 --- a/src/pages/domain/DomainSamlPage.tsx +++ b/src/pages/domain/DomainSamlPage.tsx @@ -6,10 +6,10 @@ import type {FeatureListItem} from '@components/FeatureList'; import FeatureList from '@components/FeatureList'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import RenderHTML from '@components/RenderHTML'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; +import SectionSubtitleHTML from '@components/SectionSubtitleHTML'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -100,7 +100,7 @@ function DomainSamlPage({route}: DomainSamlPageProps) { <>
} + renderSubtitle={() => } isCentralPane titleStyles={styles.accountSettingsSectionTitle} childrenStyles={[styles.gap6, styles.pt6]} @@ -136,9 +136,10 @@ function DomainSamlPage({route}: DomainSamlPageProps) { menuItems={samlFeatures} title={translate('domain.samlFeatureList.title')} renderSubtitle={() => ( - - - + )} ctaText={translate('domain.verifyDomain.title')} ctaAccessibilityLabel={translate('domain.verifyDomain.title')} diff --git a/src/pages/settings/Security/SecuritySettingsPage.tsx b/src/pages/settings/Security/SecuritySettingsPage.tsx index 0e181b06427e..f291e6c30712 100644 --- a/src/pages/settings/Security/SecuritySettingsPage.tsx +++ b/src/pages/settings/Security/SecuritySettingsPage.tsx @@ -18,8 +18,8 @@ import type {PopoverMenuItem} from '@components/PopoverMenu'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; +import SectionSubtitleHTML from '@components/SectionSubtitleHTML'; import Text from '@components/Text'; -import TextLink from '@components/TextLink'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -412,17 +412,10 @@ function SecuritySettingsPage() {
( - - {translate('delegate.copilotDelegatedAccessDescription')} - - {translate('common.learnMore')} - - . - + ${translate('common.learnMore')}.`} + subtitleMuted + /> )} isCentralPane subtitleMuted diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index f0943d2fd847..0909d70ebbb4 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -7,11 +7,11 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ImportOnyxState from '@components/ImportOnyxState'; import MenuItemList from '@components/MenuItemList'; import {useOptionsList} from '@components/OptionListContextProvider'; -import RenderHTML from '@components/RenderHTML'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import {useSearchContext} from '@components/Search/SearchContext'; import Section from '@components/Section'; +import SectionSubtitleHTML from '@components/SectionSubtitleHTML'; import SentryDebugToolMenu from '@components/SentryDebugToolMenu'; import Switch from '@components/Switch'; import TestToolMenu from '@components/TestToolMenu'; @@ -186,11 +186,7 @@ function TroubleshootPage() { illustrationContainerStyle={styles.cardSectionIllustrationContainer} illustrationBackgroundColor={colors.blue700} titleStyles={styles.accountSettingsSectionTitle} - renderSubtitle={() => ( - - - - )} + renderSubtitle={() => } // eslint-disable-next-line react/jsx-props-no-spreading {...troubleshootIllustration} > diff --git a/src/pages/workspace/reports/WorkspaceReportsPage.tsx b/src/pages/workspace/reports/WorkspaceReportsPage.tsx index cf78fefb0b23..48012701721e 100644 --- a/src/pages/workspace/reports/WorkspaceReportsPage.tsx +++ b/src/pages/workspace/reports/WorkspaceReportsPage.tsx @@ -10,10 +10,10 @@ import ImportedFromAccountingSoftware from '@components/ImportedFromAccountingSo import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import RenderHTML from '@components/RenderHTML'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; +import SectionSubtitleHTML from '@components/SectionSubtitleHTML'; import type {ListItem} from '@components/SelectionListWithSections/types'; import Text from '@components/Text'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; @@ -165,9 +165,10 @@ function WorkspaceReportFieldsPage({ const renderReportSubtitle = () => ( - - - + ); diff --git a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx index cd0965637273..a76cea096a46 100644 --- a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx +++ b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx @@ -1,10 +1,9 @@ import React, {useCallback, useMemo} from 'react'; -import {View} from 'react-native'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import RenderHTML from '@components/RenderHTML'; import Section from '@components/Section'; +import SectionSubtitleHTML from '@components/SectionSubtitleHTML'; import useCardFeeds from '@hooks/useCardFeeds'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; @@ -32,7 +31,6 @@ type IndividualExpenseRulesSectionProps = { type IndividualExpenseRulesSectionSubtitleProps = { policy?: Policy; translate: LocaleContextProps['translate']; - styles: ThemeStyles; environmentURL: string; }; @@ -43,7 +41,7 @@ type IndividualExpenseRulesMenuItem = { pendingAction?: PendingAction; }; -function IndividualExpenseRulesSectionSubtitle({policy, translate, styles, environmentURL}: IndividualExpenseRulesSectionSubtitleProps) { +function IndividualExpenseRulesSectionSubtitle({policy, translate, environmentURL}: IndividualExpenseRulesSectionSubtitleProps) { const policyID = policy?.id; const categoriesPageLink = useMemo(() => { @@ -62,11 +60,7 @@ function IndividualExpenseRulesSectionSubtitle({policy, translate, styles, envir return `${environmentURL}/${ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)}`; }, [policy?.areTagsEnabled, policyID, environmentURL]); - return ( - - - - ); + return ; } function IndividualExpenseRulesSection({policyID}: IndividualExpenseRulesSectionProps) { @@ -225,7 +219,6 @@ function IndividualExpenseRulesSection({policyID}: IndividualExpenseRulesSection )} From 30c70580a4837a60fd9c46f27ee52ada5c84b042 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Sun, 8 Mar 2026 02:06:17 +0000 Subject: [PATCH 05/18] Remove donation card title from translations in multiple languages --- src/languages/de.ts | 1 - src/languages/en.ts | 1 - src/languages/es.ts | 1 - src/languages/fr.ts | 1 - src/languages/it.ts | 1 - src/languages/ja.ts | 1 - src/languages/nl.ts | 1 - src/languages/pl.ts | 1 - src/languages/pt-BR.ts | 1 - src/languages/zh-hans.ts | 1 - 10 files changed, 10 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 5a7d85223e21..fab19b688dbf 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7507,7 +7507,6 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und description: 'Spende 1 $ an Expensify.org für jeweils 500 $, die du pro Monat ausgibst', addPaymentCardPrompt: 'Füge eine Zahlungskarte hinzu, um persönliche Karma-Spenden zu aktivieren.', stopDonationsPrompt: 'Bist du sicher? Deine monatliche Karma-Spende bewirkt viel.', - donationCardTitle: 'Für Spenden verwendete Karte', }, getInTouch: 'Ausgezeichnet! Bitte teile ihre Kontaktdaten, damit wir sie erreichen können.', introSchoolPrincipal: 'Einführung für Ihre Schulleitung', diff --git a/src/languages/en.ts b/src/languages/en.ts index 872eb77b1a77..f21dd57f80e3 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7456,7 +7456,6 @@ const translations = { description: 'Donate $1 to Expensify.org for every $500 you spend each month', addPaymentCardPrompt: 'Add a payment card to enable Personal Karma donations.', stopDonationsPrompt: 'Are you sure? Your monthly Karma donation makes a huge impact.', - donationCardTitle: 'Card used for donations', }, getInTouch: 'Excellent! Please share their information so we can get in touch with them.', introSchoolPrincipal: 'Intro to your school principal', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2f2f55661efb..5486ddad91fd 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7772,7 +7772,6 @@ ${amount} para ${merchant} - ${date}`, description: 'Dona $1 a Expensify.org por cada $500 que gastes cada mes', addPaymentCardPrompt: 'Agrega una tarjeta de pago para habilitar las donaciones de Karma personal.', stopDonationsPrompt: '¿Estás seguro? Tu donación mensual de Karma tiene un gran impacto.', - donationCardTitle: 'Tarjeta usada para donaciones', }, getInTouch: '¡Excelente! Por favor, comparte tu información para que podamos ponernos en contacto con ellos.', introSchoolPrincipal: 'Introducción al director del colegio', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index e5674920e1bc..707035bb9300 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7528,7 +7528,6 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip description: 'Faites un don de 1 $ à Expensify.org pour chaque tranche de 500 $ dépensée chaque mois', addPaymentCardPrompt: 'Ajoutez une carte de paiement pour activer les dons de Karma personnel.', stopDonationsPrompt: 'Êtes-vous sûr ? Votre don mensuel Karma a un impact considérable.', - donationCardTitle: 'Carte utilisée pour les dons', }, getInTouch: 'Excellent ! Veuillez partager leurs coordonnées afin que nous puissions les contacter.', introSchoolPrincipal: 'Présentation de la direction de votre école', diff --git a/src/languages/it.ts b/src/languages/it.ts index 7d5b38727b08..742efd9ad356 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7493,7 +7493,6 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo description: 'Dona 1 $ a Expensify.org per ogni 500 $ che spendi ogni mese', addPaymentCardPrompt: 'Aggiungi una carta di pagamento per abilitare le donazioni di Karma personale.', stopDonationsPrompt: 'Sei sicuro? La tua donazione mensile Karma ha un grande impatto.', - donationCardTitle: 'Carta usata per le donazioni', }, getInTouch: 'Eccellente! Condividi le loro informazioni così possiamo metterci in contatto con loro.', introSchoolPrincipal: 'Introduzione al dirigente scolastico', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 0927c0f82967..58bfab45a7df 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7419,7 +7419,6 @@ ${reportName} description: '毎月の支出500ドルごとに1ドルをExpensify.orgに寄付します', addPaymentCardPrompt: 'パーソナルカルマの寄付を有効にするには、支払いカードを追加してください。', stopDonationsPrompt: '本当によろしいですか?毎月のKarma寄付は大きな影響をもたらします。', - donationCardTitle: '寄付に使用するカード', }, getInTouch: '素晴らしいです!その方の情報を共有していただければ、こちらからご連絡いたします。', introSchoolPrincipal: '学校校長への紹介', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index a54093501121..ba2ea3742738 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7472,7 +7472,6 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar description: 'Doneer $1 aan Expensify.org voor elke $500 die je elke maand uitgeeft', addPaymentCardPrompt: 'Voeg een betaalkaart toe om persoonlijke karma-donaties in te schakelen.', stopDonationsPrompt: 'Weet je het zeker? Je maandelijkse Karma-donatie heeft een grote impact.', - donationCardTitle: 'Kaart gebruikt voor donaties', }, getInTouch: 'Uitstekend! Deel hun gegevens zodat we contact met hen kunnen opnemen.', introSchoolPrincipal: 'Introductie bij je schooldirecteur', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 6f7131e48482..f649a2bec8f9 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7459,7 +7459,6 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i description: 'Przekaż 1 USD na Expensify.org za każde 500 USD wydane w miesiącu', addPaymentCardPrompt: 'Dodaj kartę płatniczą, aby włączyć darowizny w ramach Osobistej Karmy.', stopDonationsPrompt: 'Czy na pewno? Twoja comiesięczna darowizna Karma ma ogromny wpływ.', - donationCardTitle: 'Karta używana do darowizn', }, getInTouch: 'Świetnie! Udostępnij ich dane kontaktowe, abyśmy mogli się z nimi skontaktować.', introSchoolPrincipal: 'Wprowadzenie dla dyrektora szkoły', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 5756ac1bff7a..f2d899e0f8b4 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7464,7 +7464,6 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e description: 'Doe US$ 1 para o Expensify.org a cada US$ 500 que você gastar por mês', addPaymentCardPrompt: 'Adicione um cartão de pagamento para ativar as doações do Karma pessoal.', stopDonationsPrompt: 'Tem certeza? Sua doação mensal de Karma causa um grande impacto.', - donationCardTitle: 'Cartão usado para doações', }, getInTouch: 'Excelente! Compartilhe as informações deles para que possamos entrar em contato.', introSchoolPrincipal: 'Apresentação ao diretor da sua escola', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 39da42ad818b..534643c44651 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7301,7 +7301,6 @@ ${reportName} description: '您每月每消费 500 美元,就向 Expensify.org 捐赠 1 美元', addPaymentCardPrompt: '请添加支付卡以启用个人 Karma 捐赠。', stopDonationsPrompt: '你确定吗?你每月的 Karma 捐赠会带来巨大影响。', - donationCardTitle: '用于捐赠的卡片', }, getInTouch: '太好了!请分享他们的联系方式,以便我们与他们取得联系。', introSchoolPrincipal: '向你们学校校长的介绍', From 94197a4f5e800513ae6da73aef3ee623153348d0 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Sun, 8 Mar 2026 02:38:58 +0000 Subject: [PATCH 06/18] Add Personal Karma feature to SaveTheWorldPage - Integrated Personal Karma toggle functionality with modals for adding payment cards and disabling donations. - Added new state management for displaying billing card details and handling user interactions. - Updated UI components to include SectionSubtitleHTML for improved subtitle rendering. - Enhanced user experience with confirmation modals for payment card actions and personal karma updates. --- src/pages/TeachersUnite/SaveTheWorldPage.tsx | 125 ++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/src/pages/TeachersUnite/SaveTheWorldPage.tsx b/src/pages/TeachersUnite/SaveTheWorldPage.tsx index ba6c499cca54..c9fe756e1974 100644 --- a/src/pages/TeachersUnite/SaveTheWorldPage.tsx +++ b/src/pages/TeachersUnite/SaveTheWorldPage.tsx @@ -1,19 +1,29 @@ -import React, {useMemo} from 'react'; +import React, {useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; +import BillingCardDetails from '@components/BillingCardDetails'; +import ConfirmModal from '@components/ConfirmModal'; +import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemList from '@components/MenuItemList'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; +import SectionSubtitleHTML from '@components/SectionSubtitleHTML'; +import Text from '@components/Text'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import Navigation from '@libs/Navigation/Navigation'; +import {doesUserHavePaymentCardAdded, getCardForSubscriptionBilling} from '@libs/SubscriptionUtils'; +import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; +import {updatePersonalKarma} from '@userActions/Subscription'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import useSaveTheWorldSectionIllustration from './useSaveTheWorldSectionIllustration'; @@ -24,8 +34,28 @@ function SaveTheWorldPage() { const waitForNavigate = useWaitForNavigation(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const theme = useTheme(); + const {isActingAsDelegate} = useDelegateNoAccessState(); + const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const illustrations = useMemoizedLazyIllustrations(['TeachersUnite']); + const [personalOffsetsEnabled = false] = useOnyx(ONYXKEYS.NVP_PERSONAL_OFFSETS); + const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID); + const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); + const [isDisablePersonalKarmaModalVisible, setIsDisablePersonalKarmaModalVisible] = useState(false); + const [isAddPaymentCardModalVisible, setIsAddPaymentCardModalVisible] = useState(false); + const shouldRevertPersonalKarmaOnAddCardModalHideRef = useRef(false); const saveTheWorldIllustration = useSaveTheWorldSectionIllustration(); + const personalKarmaTitle = translate('teachersUnitePage.personalKarma.title'); + const personalKarmaDescription = translate('teachersUnitePage.personalKarma.description'); + const personalKarmaAddPaymentCardPrompt = translate('teachersUnitePage.personalKarma.addPaymentCardPrompt'); + const personalKarmaStopDonationsPrompt = translate('teachersUnitePage.personalKarma.stopDonationsPrompt'); + const billingCard = useMemo(() => { + const userBillingCard = userBillingFundID ? fundList?.[`${userBillingFundID}`] : undefined; + if (userBillingCard?.accountData) { + return userBillingCard; + } + + return getCardForSubscriptionBilling(fundList); + }, [fundList, userBillingFundID, userBillingFundID]); const menuItems = useMemo(() => { const baseMenuItems = [ { @@ -51,6 +81,47 @@ function SaveTheWorldPage() { })); }, [translate, waitForNavigate, styles]); + const handleDisablePersonalKarma = () => { + setIsDisablePersonalKarmaModalVisible(false); + updatePersonalKarma(false); + }; + + const openAddPaymentCardPage = () => { + shouldRevertPersonalKarmaOnAddCardModalHideRef.current = false; + setIsAddPaymentCardModalVisible(false); + Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD); + }; + + const closeAddPaymentCardModal = () => { + setIsAddPaymentCardModalVisible(false); + }; + + const handleAddPaymentCardModalHide = () => { + if (!shouldRevertPersonalKarmaOnAddCardModalHideRef.current) { + return; + } + shouldRevertPersonalKarmaOnAddCardModalHideRef.current = false; + updatePersonalKarma(false); + }; + + const handlePersonalKarmaToggle = () => { + if (isActingAsDelegate) { + showDelegateNoAccessModal(); + return; + } + if (personalOffsetsEnabled) { + setIsDisablePersonalKarmaModalVisible(true); + return; + } + + updatePersonalKarma(true); + if (!billingCard) { + shouldRevertPersonalKarmaOnAddCardModalHideRef.current = true; + setIsAddPaymentCardModalVisible(true); + return; + } + }; + return (
+
( + ${translate('common.learnMore')}.`} + subtitleMuted + /> + )} + isCentralPane + titleStyles={styles.accountSettingsSectionTitle} + > + + {personalOffsetsEnabled && ( + + {billingCard?.accountData ? ( + + ) : !billingCard ? ( + {translate('subscription.cardSection.cardNotFound')} + ) : null} + + )} +
+ + setIsDisablePersonalKarmaModalVisible(false)} + prompt={personalKarmaStopDonationsPrompt} + confirmText={translate('common.disable')} + cancelText={translate('common.cancel')} + danger + /> ); } From 824cf876fbbf31caaa1ccc2eaf05012f4ab3787e Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Sun, 8 Mar 2026 03:05:55 +0000 Subject: [PATCH 07/18] Refactor imports and enhance style --- src/components/BillingCardDetails.tsx | 7 +++-- src/components/SectionSubtitleHTML.tsx | 2 +- src/pages/TeachersUnite/SaveTheWorldPage.tsx | 28 +++++++------------ .../Subscription/CardSection/CardSection.tsx | 1 - .../rules/IndividualExpenseRulesSection.tsx | 2 +- 5 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/components/BillingCardDetails.tsx b/src/components/BillingCardDetails.tsx index a3cfad16eaed..6ae398428595 100644 --- a/src/components/BillingCardDetails.tsx +++ b/src/components/BillingCardDetails.tsx @@ -2,8 +2,6 @@ import type {ReactNode} from 'react'; import React, {useMemo} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import Icon from '@components/Icon'; -import Text from '@components/Text'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; @@ -11,6 +9,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import {getPaymentMethodDescription} from '@libs/PaymentUtils'; import type Fund from '@src/types/onyx/Fund'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import Text from './Text'; +import Icon from './Icon'; type BillingCardDetailsProps = { /** The billing card data */ @@ -31,7 +32,7 @@ function BillingCardDetails({card, rightComponent, wrapperStyle}: BillingCardDet const cardMonth = useMemo(() => DateUtils.getMonthNames()[(card?.accountData?.cardMonth ?? 1) - 1], [card?.accountData?.cardMonth]); - if (!card?.accountData) { + if (isEmptyObject(card?.accountData)) { return null; } diff --git a/src/components/SectionSubtitleHTML.tsx b/src/components/SectionSubtitleHTML.tsx index 07179be3e414..ed1c0f35db62 100644 --- a/src/components/SectionSubtitleHTML.tsx +++ b/src/components/SectionSubtitleHTML.tsx @@ -2,8 +2,8 @@ import type {ComponentProps} from 'react'; import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import RenderHTML from '@components/RenderHTML'; import useThemeStyles from '@hooks/useThemeStyles'; +import RenderHTML from './RenderHTML'; type SectionSubtitleHTMLProps = { /** Subtitle HTML content */ diff --git a/src/pages/TeachersUnite/SaveTheWorldPage.tsx b/src/pages/TeachersUnite/SaveTheWorldPage.tsx index c9fe756e1974..c410315bf180 100644 --- a/src/pages/TeachersUnite/SaveTheWorldPage.tsx +++ b/src/pages/TeachersUnite/SaveTheWorldPage.tsx @@ -9,7 +9,6 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import SectionSubtitleHTML from '@components/SectionSubtitleHTML'; -import Text from '@components/Text'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -18,7 +17,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import Navigation from '@libs/Navigation/Navigation'; -import {doesUserHavePaymentCardAdded, getCardForSubscriptionBilling} from '@libs/SubscriptionUtils'; +import {getCardForSubscriptionBilling} from '@libs/SubscriptionUtils'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import {updatePersonalKarma} from '@userActions/Subscription'; import CONST from '@src/CONST'; @@ -37,9 +36,9 @@ function SaveTheWorldPage() { const {isActingAsDelegate} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const illustrations = useMemoizedLazyIllustrations(['TeachersUnite']); - const [personalOffsetsEnabled = false] = useOnyx(ONYXKEYS.NVP_PERSONAL_OFFSETS); - const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID); - const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); + const [personalOffsetsEnabled = false] = useOnyx(ONYXKEYS.NVP_PERSONAL_OFFSETS, {canBeMissing: true}); + const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID, {canBeMissing: true}); + const [fundList] = useOnyx(ONYXKEYS.FUND_LIST, {canBeMissing: true}); const [isDisablePersonalKarmaModalVisible, setIsDisablePersonalKarmaModalVisible] = useState(false); const [isAddPaymentCardModalVisible, setIsAddPaymentCardModalVisible] = useState(false); const shouldRevertPersonalKarmaOnAddCardModalHideRef = useRef(false); @@ -55,7 +54,7 @@ function SaveTheWorldPage() { } return getCardForSubscriptionBilling(fundList); - }, [fundList, userBillingFundID, userBillingFundID]); + }, [fundList, userBillingFundID]); const menuItems = useMemo(() => { const baseMenuItems = [ { @@ -118,7 +117,6 @@ function SaveTheWorldPage() { if (!billingCard) { shouldRevertPersonalKarmaOnAddCardModalHideRef.current = true; setIsAddPaymentCardModalVisible(true); - return; } }; @@ -173,19 +171,13 @@ function SaveTheWorldPage() { switchAccessibilityLabel={personalKarmaTitle} onToggle={handlePersonalKarmaToggle} isActive={personalOffsetsEnabled} - wrapperStyle={styles.mt5} + wrapperStyle={styles.mt8} /> {personalOffsetsEnabled && ( - - {billingCard?.accountData ? ( - - ) : !billingCard ? ( - {translate('subscription.cardSection.cardNotFound')} - ) : null} - + )}
diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 794c567b3229..9163706e538b 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -5,7 +5,6 @@ import ConfirmModal from '@components/ConfirmModal'; import MenuItem from '@components/MenuItem'; import RenderHTML from '@components/RenderHTML'; import Section from '@components/Section'; -import Text from '@components/Text'; import useHasTeam2025Pricing from '@hooks/useHasTeam2025Pricing'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx index a76cea096a46..55f1f4e707c8 100644 --- a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx +++ b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx @@ -15,7 +15,6 @@ import {getCashExpenseReimbursableMode, setPolicyAttendeeTrackingEnabled, setPol import {convertToDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; -import type {ThemeStyles} from '@styles/index'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -23,6 +22,7 @@ import ROUTES from '@src/ROUTES'; import type {Policy} from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {View} from 'react-native'; type IndividualExpenseRulesSectionProps = { policyID: string; From 60c8b69a5212b71bc55e4982b3bc310834994b0a Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Sun, 8 Mar 2026 03:41:53 +0000 Subject: [PATCH 08/18] Enhance BillingCardDetails component and update translations --- src/components/BillingCardDetails.tsx | 2 +- src/languages/zh-hans.ts | 2 +- src/pages/TeachersUnite/SaveTheWorldPage.tsx | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/BillingCardDetails.tsx b/src/components/BillingCardDetails.tsx index 6ae398428595..6644b62a54be 100644 --- a/src/components/BillingCardDetails.tsx +++ b/src/components/BillingCardDetails.tsx @@ -32,7 +32,7 @@ function BillingCardDetails({card, rightComponent, wrapperStyle}: BillingCardDet const cardMonth = useMemo(() => DateUtils.getMonthNames()[(card?.accountData?.cardMonth ?? 1) - 1], [card?.accountData?.cardMonth]); - if (isEmptyObject(card?.accountData)) { + if (!card?.accountData || isEmptyObject(card?.accountData)) { return null; } diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 743566fe8fad..d1686ce8be27 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7439,7 +7439,7 @@ ${reportName} title: '个人 Karma', description: '您每月每消费 500 美元,就向 Expensify.org 捐赠 1 美元', addPaymentCardPrompt: '请添加支付卡以启用个人 Karma 捐赠。', - stopDonationsPrompt: '你确定吗?你每月的 Karma 捐赠会带来巨大影响。', + stopDonationsPrompt: '您确定吗?您每月的 Karma 捐赠会带来巨大影响。', }, getInTouch: '太好了!请分享他们的联系方式,以便我们与他们取得联系。', introSchoolPrincipal: '向你们学校校长的介绍', diff --git a/src/pages/TeachersUnite/SaveTheWorldPage.tsx b/src/pages/TeachersUnite/SaveTheWorldPage.tsx index 08bb2ad59182..8d40fe41bdfd 100644 --- a/src/pages/TeachersUnite/SaveTheWorldPage.tsx +++ b/src/pages/TeachersUnite/SaveTheWorldPage.tsx @@ -36,9 +36,9 @@ function SaveTheWorldPage() { const {isActingAsDelegate} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const illustrations = useMemoizedLazyIllustrations(['TeachersUnite']); - const [personalOffsetsEnabled = false] = useOnyx(ONYXKEYS.NVP_PERSONAL_OFFSETS, {canBeMissing: true}); - const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID, {canBeMissing: true}); - const [fundList] = useOnyx(ONYXKEYS.FUND_LIST, {canBeMissing: true}); + const [personalOffsetsEnabled = false] = useOnyx(ONYXKEYS.NVP_PERSONAL_OFFSETS); + const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID); + const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); const [isDisablePersonalKarmaModalVisible, setIsDisablePersonalKarmaModalVisible] = useState(false); const [isAddPaymentCardModalVisible, setIsAddPaymentCardModalVisible] = useState(false); const shouldRevertPersonalKarmaOnAddCardModalHideRef = useRef(false); From d6036ce2214c5a6f73c55e0eb69890517d8e886d Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Tue, 10 Mar 2026 01:44:58 +0000 Subject: [PATCH 09/18] Add PaymentCardDetails component and update references in SaveTheWorldPage and CardSection - Introduced a new PaymentCardDetails component for displaying billing card information. - Replaced instances of BillingCardDetails with PaymentCardDetails in SaveTheWorldPage and CardSection for consistency and improved functionality. --- .../{BillingCardDetails.tsx => PaymentCardDetails.tsx} | 6 +++--- src/pages/TeachersUnite/SaveTheWorldPage.tsx | 4 ++-- src/pages/settings/Subscription/CardSection/CardSection.tsx | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) rename src/components/{BillingCardDetails.tsx => PaymentCardDetails.tsx} (92%) diff --git a/src/components/BillingCardDetails.tsx b/src/components/PaymentCardDetails.tsx similarity index 92% rename from src/components/BillingCardDetails.tsx rename to src/components/PaymentCardDetails.tsx index 6644b62a54be..b190a0e4a8e2 100644 --- a/src/components/BillingCardDetails.tsx +++ b/src/components/PaymentCardDetails.tsx @@ -13,7 +13,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import Text from './Text'; import Icon from './Icon'; -type BillingCardDetailsProps = { +type PaymentCardDetailsProps = { /** The billing card data */ card?: Fund; @@ -24,7 +24,7 @@ type BillingCardDetailsProps = { wrapperStyle?: StyleProp; }; -function BillingCardDetails({card, rightComponent, wrapperStyle}: BillingCardDetailsProps) { +function PaymentCardDetails({card, rightComponent, wrapperStyle}: PaymentCardDetailsProps) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); @@ -55,4 +55,4 @@ function BillingCardDetails({card, rightComponent, wrapperStyle}: BillingCardDet ); } -export default BillingCardDetails; +export default PaymentCardDetails; diff --git a/src/pages/TeachersUnite/SaveTheWorldPage.tsx b/src/pages/TeachersUnite/SaveTheWorldPage.tsx index 8d40fe41bdfd..42149f23a5c3 100644 --- a/src/pages/TeachersUnite/SaveTheWorldPage.tsx +++ b/src/pages/TeachersUnite/SaveTheWorldPage.tsx @@ -1,6 +1,6 @@ import React, {useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; -import BillingCardDetails from '@components/BillingCardDetails'; +import PaymentCardDetails from '@components/PaymentCardDetails'; import ConfirmModal from '@components/ConfirmModal'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -175,7 +175,7 @@ function SaveTheWorldPage() { wrapperStyle={styles.mt8} /> {personalOffsetsEnabled && ( - diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index 9922015bd7c8..bda304902647 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -1,6 +1,6 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; -import BillingCardDetails from '@components/BillingCardDetails'; +import PaymentCardDetails from '@components/PaymentCardDetails'; import MenuItem from '@components/MenuItem'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import RenderHTML from '@components/RenderHTML'; @@ -215,7 +215,7 @@ function CardSection() { > {!isEmptyObject(defaultCard?.accountData) && ( - } /> From e4dc32599bd398999d22fa29fcb553fa0646501f Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Wed, 11 Mar 2026 23:39:43 +0000 Subject: [PATCH 10/18] Update personal karma copy --- src/languages/de.ts | 7 +++---- src/languages/en.ts | 5 ++--- src/languages/es.ts | 5 ++--- src/languages/fr.ts | 5 ++--- src/languages/it.ts | 5 ++--- src/languages/ja.ts | 7 +++---- src/languages/nl.ts | 5 ++--- src/languages/pl.ts | 5 ++--- src/languages/pt-BR.ts | 7 +++---- src/languages/zh-hans.ts | 5 ++--- 10 files changed, 23 insertions(+), 33 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 01628ea949f6..a41ec9a191c8 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7659,10 +7659,9 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und iKnowATeacher: 'Ich kenne eine Lehrkraft', iAmATeacher: 'Ich bin Lehrer', personalKarma: { - title: 'Persönliches Karma', - description: 'Spende 1 $ an Expensify.org für jeweils 500 $, die du pro Monat ausgibst', - addPaymentCardPrompt: 'Füge eine Zahlungskarte hinzu, um persönliche Karma-Spenden zu aktivieren.', - stopDonationsPrompt: 'Bist du sicher? Deine monatliche Karma-Spende bewirkt viel.', + title: 'Persönliches Karma aktivieren', + description: 'Spende 1 $ an Expensify.org für je 500 $, die du jeden Monat ausgibst', + stopDonationsPrompt: 'Bist du sicher, dass du nicht mehr an Expensify.org spenden möchtest?', }, getInTouch: 'Ausgezeichnet! Bitte teile ihre Kontaktdaten, damit wir sie erreichen können.', introSchoolPrincipal: 'Einführung für Ihre Schulleitung', diff --git a/src/languages/en.ts b/src/languages/en.ts index d48602a17920..9be52d06c55a 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7620,10 +7620,9 @@ const translations = { iKnowATeacher: 'I know a teacher', iAmATeacher: 'I am a teacher', personalKarma: { - title: 'Personal Karma', + title: 'Enable Personal Karma', description: 'Donate $1 to Expensify.org for every $500 you spend each month', - addPaymentCardPrompt: 'Add a payment card to enable Personal Karma donations.', - stopDonationsPrompt: 'Are you sure? Your monthly Karma donation makes a huge impact.', + stopDonationsPrompt: 'Are you sure you want to stop donating to Expensify.org?', }, getInTouch: 'Excellent! Please share their information so we can get in touch with them.', introSchoolPrincipal: 'Intro to your school principal', diff --git a/src/languages/es.ts b/src/languages/es.ts index bd8abf2a9285..b65d38eb8cdf 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7937,10 +7937,9 @@ ${amount} para ${merchant} - ${date}`, iKnowATeacher: 'Yo conozco a un profesor', iAmATeacher: 'Soy profesor', personalKarma: { - title: 'Karma personal', + title: 'Activar Karma personal', description: 'Dona $1 a Expensify.org por cada $500 que gastes cada mes', - addPaymentCardPrompt: 'Agrega una tarjeta de pago para habilitar las donaciones de Karma personal.', - stopDonationsPrompt: '¿Estás seguro? Tu donación mensual de Karma tiene un gran impacto.', + stopDonationsPrompt: '¿Seguro que quieres dejar de donar a Expensify.org?', }, getInTouch: '¡Excelente! Por favor, comparte tu información para que podamos ponernos en contacto con ellos.', introSchoolPrincipal: 'Introducción al director del colegio', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 61c1d6745431..3449f3a14ba3 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7681,10 +7681,9 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip iKnowATeacher: 'Je connais un enseignant', iAmATeacher: 'Je suis enseignant', personalKarma: { - title: 'Karma personnel', + title: 'Activer le Karma personnel', description: 'Faites un don de 1 $ à Expensify.org pour chaque tranche de 500 $ dépensée chaque mois', - addPaymentCardPrompt: 'Ajoutez une carte de paiement pour activer les dons de Karma personnel.', - stopDonationsPrompt: 'Êtes-vous sûr ? Votre don mensuel Karma a un impact considérable.', + stopDonationsPrompt: 'Êtes-vous sûr de vouloir arrêter de faire des dons à Expensify.org ?', }, getInTouch: 'Excellent ! Veuillez partager leurs coordonnées afin que nous puissions les contacter.', introSchoolPrincipal: 'Présentation de la direction de votre école', diff --git a/src/languages/it.ts b/src/languages/it.ts index b2a9ac3f9ab4..d3f57d3e5882 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7645,10 +7645,9 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo iKnowATeacher: 'Conosco un insegnante', iAmATeacher: 'Sono un insegnante', personalKarma: { - title: 'Karma personale', + title: 'Attiva il Karma personale', description: 'Dona 1 $ a Expensify.org per ogni 500 $ che spendi ogni mese', - addPaymentCardPrompt: 'Aggiungi una carta di pagamento per abilitare le donazioni di Karma personale.', - stopDonationsPrompt: 'Sei sicuro? La tua donazione mensile Karma ha un grande impatto.', + stopDonationsPrompt: 'Sei sicuro di voler smettere di donare a Expensify.org?', }, getInTouch: 'Eccellente! Condividi le loro informazioni così possiamo metterci in contatto con loro.', introSchoolPrincipal: 'Introduzione al dirigente scolastico', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 1877fbe0d2e0..f2496e91c040 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7560,10 +7560,9 @@ ${reportName} iKnowATeacher: '私は先生を知っています', iAmATeacher: '私は教師です', personalKarma: { - title: 'パーソナルカルマ', - description: '毎月の支出500ドルごとに1ドルをExpensify.orgに寄付します', - addPaymentCardPrompt: 'パーソナルカルマの寄付を有効にするには、支払いカードを追加してください。', - stopDonationsPrompt: '本当によろしいですか?毎月のKarma寄付は大きな影響をもたらします。', + title: 'パーソナルカルマを有効にする', + description: '毎月の支出500ドルごとに1ドルを Expensify.org に寄付します', + stopDonationsPrompt: 'Expensify.org への寄付をやめてもよろしいですか?', }, getInTouch: '素晴らしいです!その方の情報を共有していただければ、こちらからご連絡いたします。', introSchoolPrincipal: '学校校長への紹介', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 3d2cfd208839..9dbbc6c83ce2 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7624,10 +7624,9 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar iKnowATeacher: 'Ik ken een leraar', iAmATeacher: 'Ik ben een leraar', personalKarma: { - title: 'Persoonlijke karma', + title: 'Persoonlijke karma inschakelen', description: 'Doneer $1 aan Expensify.org voor elke $500 die je elke maand uitgeeft', - addPaymentCardPrompt: 'Voeg een betaalkaart toe om persoonlijke karma-donaties in te schakelen.', - stopDonationsPrompt: 'Weet je het zeker? Je maandelijkse Karma-donatie heeft een grote impact.', + stopDonationsPrompt: 'Weet je zeker dat je wilt stoppen met doneren aan Expensify.org?', }, getInTouch: 'Uitstekend! Deel hun gegevens zodat we contact met hen kunnen opnemen.', introSchoolPrincipal: 'Introductie bij je schooldirecteur', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index c002ae6cbd02..b4655e660821 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7612,10 +7612,9 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i iKnowATeacher: 'Znam nauczyciela', iAmATeacher: 'Jestem nauczycielem', personalKarma: { - title: 'Karma osobista', + title: 'Włącz osobistą karmę', description: 'Przekaż 1 USD na Expensify.org za każde 500 USD wydane w miesiącu', - addPaymentCardPrompt: 'Dodaj kartę płatniczą, aby włączyć darowizny w ramach Osobistej Karmy.', - stopDonationsPrompt: 'Czy na pewno? Twoja comiesięczna darowizna Karma ma ogromny wpływ.', + stopDonationsPrompt: 'Czy na pewno chcesz przestać przekazywać darowizny na rzecz Expensify.org?', }, getInTouch: 'Świetnie! Udostępnij ich dane kontaktowe, abyśmy mogli się z nimi skontaktować.', introSchoolPrincipal: 'Wprowadzenie dla dyrektora szkoły', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 1237b4db5e55..6d5e18c59c37 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7616,10 +7616,9 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e iKnowATeacher: 'Eu conheço um professor', iAmATeacher: 'Sou professor(a)', personalKarma: { - title: 'Karma pessoal', - description: 'Doe US$ 1 para o Expensify.org a cada US$ 500 que você gastar por mês', - addPaymentCardPrompt: 'Adicione um cartão de pagamento para ativar as doações do Karma pessoal.', - stopDonationsPrompt: 'Tem certeza? Sua doação mensal de Karma causa um grande impacto.', + title: 'Ativar Karma pessoal', + description: 'Doe US$ 1 para Expensify.org a cada US$ 500 que você gastar por mês', + stopDonationsPrompt: 'Tem certeza de que deseja parar de doar para Expensify.org?', }, getInTouch: 'Excelente! Compartilhe as informações deles para que possamos entrar em contato.', introSchoolPrincipal: 'Apresentação ao diretor da sua escola', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 70b13313bb8f..2f02245c9faf 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7440,10 +7440,9 @@ ${reportName} iKnowATeacher: '我认识一位老师', iAmATeacher: '我是老师', personalKarma: { - title: '个人 Karma', + title: '启用个人 Karma', description: '您每月每消费 500 美元,就向 Expensify.org 捐赠 1 美元', - addPaymentCardPrompt: '请添加支付卡以启用个人 Karma 捐赠。', - stopDonationsPrompt: '您确定吗?您每月的 Karma 捐赠会带来巨大影响。', + stopDonationsPrompt: '确定要停止向 Expensify.org 捐款吗?', }, getInTouch: '太好了!请分享他们的联系方式,以便我们与他们取得联系。', introSchoolPrincipal: '向你们学校校长的介绍', From 9ab06294b7fb6351ffc7d692560075e8084aa8b5 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Wed, 11 Mar 2026 23:40:24 +0000 Subject: [PATCH 11/18] Add new route and screen for adding payment card in Save The World feature - Introduced SETTINGS_SAVE_THE_WORLD_ADD_PAYMENT_CARD route in ROUTES.ts. - Added ADD_PAYMENT_CARD screen to SCREENS.ts. - Updated TeachersUniteNavigator to include the new ADD_PAYMENT_CARD screen. - Modified linking configuration to route to the new screen. - Adjusted SaveTheWorldPage to navigate to the new payment card screen and removed the old modal implementation. --- src/ROUTES.ts | 1 + src/SCREENS.ts | 1 + .../ModalStackNavigators/index.tsx | 1 + .../RELATIONS/SETTINGS_TO_RHP.ts | 2 +- src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 1 + src/pages/TeachersUnite/SaveTheWorldPage.tsx | 48 +++++-------------- 7 files changed, 19 insertions(+), 36 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index cbca11029f2e..acc3a5d4294b 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -632,6 +632,7 @@ const ROUTES = { }, SETTINGS_SAVE_THE_WORLD: 'settings/teachersunite', + SETTINGS_SAVE_THE_WORLD_ADD_PAYMENT_CARD: 'settings/teachersunite/add-payment-card', KEYBOARD_SHORTCUTS: { route: 'keyboard-shortcuts', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index b874dc62d60e..f5e4a7bddcda 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -267,6 +267,7 @@ const SCREENS = { }, SAVE_THE_WORLD: { ROOT: 'SaveTheWorld_Root', + ADD_PAYMENT_CARD: 'SaveTheWorld_Add_Payment_Card', }, RIGHT_MODAL: { SETTINGS: 'Settings', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index f5adce765a74..880aaadf404b 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -380,6 +380,7 @@ const NewTaskModalStackNavigator = createModalStackNavigator({ [SCREENS.SAVE_THE_WORLD.ROOT]: () => require('../../../../pages/TeachersUnite/SaveTheWorldPage').default, + [SCREENS.SAVE_THE_WORLD.ADD_PAYMENT_CARD]: () => require('../../../../pages/settings/Subscription/PaymentCard').default, [SCREENS.I_KNOW_A_TEACHER]: () => require('../../../../pages/TeachersUnite/KnowATeacherPage').default, [SCREENS.INTRO_SCHOOL_PRINCIPAL]: () => require('../../../../pages/TeachersUnite/ImTeacherPage').default, [SCREENS.I_AM_A_TEACHER]: () => require('../../../../pages/TeachersUnite/ImTeacherPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts index a060628504df..e220b3705dbc 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -106,7 +106,7 @@ const SETTINGS_TO_RHP: Partial['config'] = { }, [SCREENS.RIGHT_MODAL.TEACHERS_UNITE]: { screens: { + [SCREENS.SAVE_THE_WORLD.ADD_PAYMENT_CARD]: ROUTES.SETTINGS_SAVE_THE_WORLD_ADD_PAYMENT_CARD, [SCREENS.I_KNOW_A_TEACHER]: ROUTES.I_KNOW_A_TEACHER, [SCREENS.INTRO_SCHOOL_PRINCIPAL]: ROUTES.INTRO_SCHOOL_PRINCIPAL, [SCREENS.I_AM_A_TEACHER]: ROUTES.I_AM_A_TEACHER, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index c3390f796b91..b20099625758 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2196,6 +2196,7 @@ type NewTaskNavigatorParamList = { type TeachersUniteNavigatorParamList = { [SCREENS.SAVE_THE_WORLD.ROOT]: undefined; + [SCREENS.SAVE_THE_WORLD.ADD_PAYMENT_CARD]: undefined; [SCREENS.I_KNOW_A_TEACHER]: undefined; [SCREENS.INTRO_SCHOOL_PRINCIPAL]: undefined; [SCREENS.I_AM_A_TEACHER]: undefined; diff --git a/src/pages/TeachersUnite/SaveTheWorldPage.tsx b/src/pages/TeachersUnite/SaveTheWorldPage.tsx index 42149f23a5c3..48f1aa2e72a0 100644 --- a/src/pages/TeachersUnite/SaveTheWorldPage.tsx +++ b/src/pages/TeachersUnite/SaveTheWorldPage.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useRef, useState} from 'react'; +import React, {useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import PaymentCardDetails from '@components/PaymentCardDetails'; import ConfirmModal from '@components/ConfirmModal'; @@ -40,12 +40,10 @@ function SaveTheWorldPage() { const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID); const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); const [isDisablePersonalKarmaModalVisible, setIsDisablePersonalKarmaModalVisible] = useState(false); - const [isAddPaymentCardModalVisible, setIsAddPaymentCardModalVisible] = useState(false); - const shouldRevertPersonalKarmaOnAddCardModalHideRef = useRef(false); + const pendingPersonalKarmaEnableRef = useRef(false); const saveTheWorldIllustration = useSaveTheWorldSectionIllustration(); const personalKarmaTitle = translate('teachersUnitePage.personalKarma.title'); const personalKarmaDescription = translate('teachersUnitePage.personalKarma.description'); - const personalKarmaAddPaymentCardPrompt = translate('teachersUnitePage.personalKarma.addPaymentCardPrompt'); const personalKarmaStopDonationsPrompt = translate('teachersUnitePage.personalKarma.stopDonationsPrompt'); const billingCard = useMemo(() => { const userBillingCard = userBillingFundID ? fundList?.[`${userBillingFundID}`] : undefined; @@ -80,29 +78,18 @@ function SaveTheWorldPage() { })); }, [translate, waitForNavigate, styles]); + useEffect(() => { + if (pendingPersonalKarmaEnableRef.current && billingCard) { + pendingPersonalKarmaEnableRef.current = false; + updatePersonalKarma(true); + } + }, [billingCard]); + const handleDisablePersonalKarma = () => { setIsDisablePersonalKarmaModalVisible(false); updatePersonalKarma(false); }; - const openAddPaymentCardPage = () => { - shouldRevertPersonalKarmaOnAddCardModalHideRef.current = false; - setIsAddPaymentCardModalVisible(false); - Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD); - }; - - const closeAddPaymentCardModal = () => { - setIsAddPaymentCardModalVisible(false); - }; - - const handleAddPaymentCardModalHide = () => { - if (!shouldRevertPersonalKarmaOnAddCardModalHideRef.current) { - return; - } - shouldRevertPersonalKarmaOnAddCardModalHideRef.current = false; - updatePersonalKarma(false); - }; - const handlePersonalKarmaToggle = () => { if (isActingAsDelegate) { showDelegateNoAccessModal(); @@ -113,11 +100,12 @@ function SaveTheWorldPage() { return; } - updatePersonalKarma(true); if (!billingCard) { - shouldRevertPersonalKarmaOnAddCardModalHideRef.current = true; - setIsAddPaymentCardModalVisible(true); + pendingPersonalKarmaEnableRef.current = true; + Navigation.navigate(ROUTES.SETTINGS_SAVE_THE_WORLD_ADD_PAYMENT_CARD); + return; } + updatePersonalKarma(true); }; return ( @@ -183,16 +171,6 @@ function SaveTheWorldPage() { - Date: Thu, 12 Mar 2026 00:40:50 +0000 Subject: [PATCH 12/18] Fix prettier formatting and ESLint prefer-early-return error Co-authored-by: Fedi Rajhi --- src/components/PaymentCardDetails.tsx | 2 +- src/pages/TeachersUnite/SaveTheWorldPage.tsx | 9 +++++---- .../settings/Subscription/CardSection/CardSection.tsx | 2 +- .../workspace/rules/IndividualExpenseRulesSection.tsx | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/PaymentCardDetails.tsx b/src/components/PaymentCardDetails.tsx index b190a0e4a8e2..8ee13173f1a0 100644 --- a/src/components/PaymentCardDetails.tsx +++ b/src/components/PaymentCardDetails.tsx @@ -10,8 +10,8 @@ import DateUtils from '@libs/DateUtils'; import {getPaymentMethodDescription} from '@libs/PaymentUtils'; import type Fund from '@src/types/onyx/Fund'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import Text from './Text'; import Icon from './Icon'; +import Text from './Text'; type PaymentCardDetailsProps = { /** The billing card data */ diff --git a/src/pages/TeachersUnite/SaveTheWorldPage.tsx b/src/pages/TeachersUnite/SaveTheWorldPage.tsx index 48f1aa2e72a0..5ae26b616d84 100644 --- a/src/pages/TeachersUnite/SaveTheWorldPage.tsx +++ b/src/pages/TeachersUnite/SaveTheWorldPage.tsx @@ -1,10 +1,10 @@ import React, {useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; -import PaymentCardDetails from '@components/PaymentCardDetails'; import ConfirmModal from '@components/ConfirmModal'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemList from '@components/MenuItemList'; +import PaymentCardDetails from '@components/PaymentCardDetails'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; @@ -79,10 +79,11 @@ function SaveTheWorldPage() { }, [translate, waitForNavigate, styles]); useEffect(() => { - if (pendingPersonalKarmaEnableRef.current && billingCard) { - pendingPersonalKarmaEnableRef.current = false; - updatePersonalKarma(true); + if (!pendingPersonalKarmaEnableRef.current || !billingCard) { + return; } + pendingPersonalKarmaEnableRef.current = false; + updatePersonalKarma(true); }, [billingCard]); const handleDisablePersonalKarma = () => { diff --git a/src/pages/settings/Subscription/CardSection/CardSection.tsx b/src/pages/settings/Subscription/CardSection/CardSection.tsx index bda304902647..0f5482e18b7d 100644 --- a/src/pages/settings/Subscription/CardSection/CardSection.tsx +++ b/src/pages/settings/Subscription/CardSection/CardSection.tsx @@ -1,8 +1,8 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; -import PaymentCardDetails from '@components/PaymentCardDetails'; import MenuItem from '@components/MenuItem'; import {ModalActions} from '@components/Modal/Global/ModalContext'; +import PaymentCardDetails from '@components/PaymentCardDetails'; import RenderHTML from '@components/RenderHTML'; import Section from '@components/Section'; import useConfirmModal from '@hooks/useConfirmModal'; diff --git a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx index 8edfb5e00012..7d7d0c89f3fd 100644 --- a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx +++ b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx @@ -1,4 +1,5 @@ import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -22,7 +23,6 @@ import ROUTES from '@src/ROUTES'; import type {Policy} from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {View} from 'react-native'; type IndividualExpenseRulesSectionProps = { policyID: string; From d6e90156c16c3944b0f202d8e113f7c81e6b8b45 Mon Sep 17 00:00:00 2001 From: "Fedi Rajhi (via MelvinBot)" Date: Sun, 15 Mar 2026 01:22:56 +0000 Subject: [PATCH 13/18] Remove unused useCardFeeds import Co-authored-by: Fedi Rajhi --- src/pages/workspace/rules/IndividualExpenseRulesSection.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx index bac221784d42..b6f715912446 100644 --- a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx +++ b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx @@ -5,7 +5,6 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Section from '@components/Section'; import SectionSubtitleHTML from '@components/SectionSubtitleHTML'; -import useCardFeeds from '@hooks/useCardFeeds'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; From b64503aa7490d71168853da1d030801b032eb3da Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Mon, 23 Mar 2026 18:25:44 +0000 Subject: [PATCH 14/18] feat: add openSaveTheWorldPage command and corresponding API integration --- src/libs/API/types.ts | 2 ++ src/libs/actions/Subscription.ts | 8 ++++++++ src/pages/TeachersUnite/SaveTheWorldPage.tsx | 6 +++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index e76a26b81b91..fbf779803cc1 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -1227,6 +1227,7 @@ const READ_COMMANDS = { OPEN_DUPLICATE_POLICY_PAGE: 'OpenDuplicatePolicyPage', OPEN_POLICY_INITIAL_PAGE: 'OpenPolicyInitialPage', OPEN_SUBSCRIPTION_PAGE: 'OpenSubscriptionPage', + OPEN_SAVE_THE_WORLD_PAGE: 'OpenSaveTheWorldPage', OPEN_DRAFT_DISTANCE_EXPENSE: 'OpenDraftDistanceExpense', START_ISSUE_NEW_CARD_FLOW: 'StartIssueNewCardFlow', OPEN_CARD_DETAILS_PAGE: 'OpenCardDetailsPage', @@ -1317,6 +1318,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_POLICY_INITIAL_PAGE]: Parameters.OpenPolicyInitialPageParams; [READ_COMMANDS.OPEN_POLICY_RECEIPT_PARTNERS_PAGE]: Parameters.OpenPolicyReceiptPartnersPageParams; [READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE]: null; + [READ_COMMANDS.OPEN_SAVE_THE_WORLD_PAGE]: null; [READ_COMMANDS.OPEN_DRAFT_DISTANCE_EXPENSE]: null; [READ_COMMANDS.START_ISSUE_NEW_CARD_FLOW]: Parameters.StartIssueNewCardFlowParams; [READ_COMMANDS.OPEN_CARD_DETAILS_PAGE]: Parameters.OpenCardDetailsPageParams; diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index 71f74484f5f7..6cfa50699168 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -44,6 +44,13 @@ function openSubscriptionPage() { API.read(READ_COMMANDS.OPEN_SUBSCRIPTION_PAGE, null, {optimisticData, successData, failureData}); } +/** + * Fetches data when the user opens the Save The World page + */ +function openSaveTheWorldPage() { + API.read(READ_COMMANDS.OPEN_SAVE_THE_WORLD_PAGE, null); +} + function updateSubscriptionType(type: SubscriptionType) { const optimisticData: Array> = [ { @@ -402,6 +409,7 @@ function applyExpensifyCode(promoCode: string) { export { openSubscriptionPage, + openSaveTheWorldPage, updateSubscriptionAutoRenew, updateSubscriptionAddNewUsersAutomatically, updatePersonalKarma, diff --git a/src/pages/TeachersUnite/SaveTheWorldPage.tsx b/src/pages/TeachersUnite/SaveTheWorldPage.tsx index dea87a2843d6..3c52e72371b4 100644 --- a/src/pages/TeachersUnite/SaveTheWorldPage.tsx +++ b/src/pages/TeachersUnite/SaveTheWorldPage.tsx @@ -20,7 +20,7 @@ import useWaitForNavigation from '@hooks/useWaitForNavigation'; import Navigation from '@libs/Navigation/Navigation'; import {getCardForSubscriptionBilling} from '@libs/SubscriptionUtils'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; -import {updatePersonalKarma} from '@userActions/Subscription'; +import {openSaveTheWorldPage, updatePersonalKarma} from '@userActions/Subscription'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -80,6 +80,10 @@ function SaveTheWorldPage() { })); }, [translate, waitForNavigate, styles]); + useEffect(() => { + openSaveTheWorldPage(); + }, []); + useEffect(() => { if (!pendingPersonalKarmaEnableRef.current || !billingCard) { return; From b30b75f13c3e90d371698d18bb159c9e552be31c Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Mon, 6 Apr 2026 15:47:17 +0100 Subject: [PATCH 15/18] chore: Remove unnecessary useMemo and update SaveTheWorldPage to utilize useIsFocused for effect dependencies --- src/components/PaymentCardDetails.tsx | 4 ++-- src/pages/TeachersUnite/SaveTheWorldPage.tsx | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/PaymentCardDetails.tsx b/src/components/PaymentCardDetails.tsx index 8ee13173f1a0..ec1d9f028928 100644 --- a/src/components/PaymentCardDetails.tsx +++ b/src/components/PaymentCardDetails.tsx @@ -1,5 +1,5 @@ import type {ReactNode} from 'react'; -import React, {useMemo} from 'react'; +import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -30,7 +30,7 @@ function PaymentCardDetails({card, rightComponent, wrapperStyle}: PaymentCardDet const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['CreditCard']); - const cardMonth = useMemo(() => DateUtils.getMonthNames()[(card?.accountData?.cardMonth ?? 1) - 1], [card?.accountData?.cardMonth]); + const cardMonth = DateUtils.getMonthNames()[(card?.accountData?.cardMonth ?? 1) - 1]; if (!card?.accountData || isEmptyObject(card?.accountData)) { return null; diff --git a/src/pages/TeachersUnite/SaveTheWorldPage.tsx b/src/pages/TeachersUnite/SaveTheWorldPage.tsx index 3c52e72371b4..8db94d4d822f 100644 --- a/src/pages/TeachersUnite/SaveTheWorldPage.tsx +++ b/src/pages/TeachersUnite/SaveTheWorldPage.tsx @@ -1,3 +1,4 @@ +import {useIsFocused} from '@react-navigation/native'; import React, {useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import ConfirmModal from '@components/ConfirmModal'; @@ -84,13 +85,19 @@ function SaveTheWorldPage() { openSaveTheWorldPage(); }, []); + const isFocused = useIsFocused(); + useEffect(() => { - if (!pendingPersonalKarmaEnableRef.current || !billingCard) { + if (!isFocused || !pendingPersonalKarmaEnableRef.current) { return; } + pendingPersonalKarmaEnableRef.current = false; - updatePersonalKarma(true); - }, [billingCard]); + + if (billingCard) { + updatePersonalKarma(true); + } + }, [isFocused, billingCard]); const handleDisablePersonalKarma = () => { setIsDisablePersonalKarmaModalVisible(false); From 82b996d2316cee3590dfed2c39eb2a95929581c6 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Mon, 6 Apr 2026 19:45:20 +0100 Subject: [PATCH 16/18] chore: Add "PINATM" to cspell.json dictionary --- cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell.json b/cspell.json index 1520c484dd61..5930b1473dae 100644 --- a/cspell.json +++ b/cspell.json @@ -553,6 +553,7 @@ "Picklist", "picklists", "PINGPONG", + "PINATM", "pkill", "Pluginfile", "pluralrules", From be8f82d1d44562995f1c52d2f975e95d3e6c0fa1 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Wed, 8 Apr 2026 18:39:35 +0100 Subject: [PATCH 17/18] feat: Add optimistic UI state for personal karma updates --- src/ONYXKEYS.ts | 4 +++ src/libs/actions/Subscription.ts | 23 ++++++++++-- src/pages/TeachersUnite/SaveTheWorldPage.tsx | 38 ++++++++++---------- 3 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ac27d95a6bab..853f49bc361e 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -393,6 +393,9 @@ const ONYXKEYS = { /** Set when we are loading fresh subscription/billing data from the server */ IS_LOADING_SUBSCRIPTION_DATA: 'isLoadingSubscriptionData', + /** Set while UpdatePersonalKarma is in flight (optimistic UI for Save The World toggle) */ + IS_PENDING_UPDATE_PERSONAL_KARMA: 'isPendingUpdatePersonalKarma', + /** Set whether we are loading the search filters card data */ IS_SEARCH_FILTERS_CARD_DATA_LOADED: 'isSearchFiltersCardDataLoaded', @@ -1406,6 +1409,7 @@ type OnyxValuesMapping = { [ONYXKEYS.RAM_ONLY_IS_LOADING_REPORT_DATA]: boolean; [ONYXKEYS.IS_SEARCH_FILTERS_CARD_DATA_LOADED]: boolean; [ONYXKEYS.IS_LOADING_SUBSCRIPTION_DATA]: boolean; + [ONYXKEYS.IS_PENDING_UPDATE_PERSONAL_KARMA]: boolean; [ONYXKEYS.IS_SEARCH_PAGE_DATA_LOADED]: boolean; [ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN]: boolean; [ONYXKEYS.RAM_ONLY_IS_LOADING_APP]: boolean; diff --git a/src/libs/actions/Subscription.ts b/src/libs/actions/Subscription.ts index 6cfa50699168..f189281fccfc 100644 --- a/src/libs/actions/Subscription.ts +++ b/src/libs/actions/Subscription.ts @@ -211,7 +211,12 @@ function updateSubscriptionAddNewUsersAutomatically(addNewUsersAutomatically: bo } function updatePersonalKarma(enabled: boolean) { - const optimisticData: Array> = [ + const optimisticData: Array | OnyxUpdate> = [ + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.IS_PENDING_UPDATE_PERSONAL_KARMA, + value: true, + }, { onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.NVP_PERSONAL_OFFSETS, @@ -219,7 +224,20 @@ function updatePersonalKarma(enabled: boolean) { }, ]; - const failureData: Array> = [ + const successData: Array> = [ + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.IS_PENDING_UPDATE_PERSONAL_KARMA, + value: false, + }, + ]; + + const failureData: Array | OnyxUpdate> = [ + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.IS_PENDING_UPDATE_PERSONAL_KARMA, + value: false, + }, { onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.NVP_PERSONAL_OFFSETS, @@ -233,6 +251,7 @@ function updatePersonalKarma(enabled: boolean) { API.write(WRITE_COMMANDS.UPDATE_PERSONAL_KARMA, parameters, { optimisticData, + successData, failureData, }); } diff --git a/src/pages/TeachersUnite/SaveTheWorldPage.tsx b/src/pages/TeachersUnite/SaveTheWorldPage.tsx index 8db94d4d822f..da4dadf463da 100644 --- a/src/pages/TeachersUnite/SaveTheWorldPage.tsx +++ b/src/pages/TeachersUnite/SaveTheWorldPage.tsx @@ -1,15 +1,16 @@ import {useIsFocused} from '@react-navigation/native'; -import React, {useEffect, useMemo, useRef, useState} from 'react'; +import React, {useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; -import ConfirmModal from '@components/ConfirmModal'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemList from '@components/MenuItemList'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import PaymentCardDetails from '@components/PaymentCardDetails'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import SectionSubtitleHTML from '@components/SectionSubtitleHTML'; +import useConfirmModal from '@hooks/useConfirmModal'; import useDocumentTitle from '@hooks/useDocumentTitle'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -42,7 +43,8 @@ function SaveTheWorldPage() { const [personalOffsetsEnabled = false] = useOnyx(ONYXKEYS.NVP_PERSONAL_OFFSETS); const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID); const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); - const [isDisablePersonalKarmaModalVisible, setIsDisablePersonalKarmaModalVisible] = useState(false); + const [isPendingUpdatePersonalKarma = false] = useOnyx(ONYXKEYS.IS_PENDING_UPDATE_PERSONAL_KARMA); + const {showConfirmModal} = useConfirmModal(); const pendingPersonalKarmaEnableRef = useRef(false); const saveTheWorldIllustration = useSaveTheWorldSectionIllustration(); const personalKarmaTitle = translate('teachersUnitePage.personalKarma.title'); @@ -99,18 +101,24 @@ function SaveTheWorldPage() { } }, [isFocused, billingCard]); - const handleDisablePersonalKarma = () => { - setIsDisablePersonalKarmaModalVisible(false); - updatePersonalKarma(false); - }; - const handlePersonalKarmaToggle = () => { if (isActingAsDelegate) { showDelegateNoAccessModal(); return; } if (personalOffsetsEnabled) { - setIsDisablePersonalKarmaModalVisible(true); + showConfirmModal({ + title: personalKarmaTitle, + prompt: personalKarmaStopDonationsPrompt, + confirmText: translate('common.disable'), + cancelText: translate('common.cancel'), + danger: true, + }).then(({action}) => { + if (action !== ModalActions.CONFIRM) { + return; + } + updatePersonalKarma(false); + }); return; } @@ -174,6 +182,8 @@ function SaveTheWorldPage() { switchAccessibilityLabel={personalKarmaTitle} onToggle={handlePersonalKarmaToggle} isActive={personalOffsetsEnabled} + pendingAction={isPendingUpdatePersonalKarma ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : undefined} + disabled={isPendingUpdatePersonalKarma} wrapperStyle={styles.mt8} /> {personalOffsetsEnabled && ( @@ -185,16 +195,6 @@ function SaveTheWorldPage() { - setIsDisablePersonalKarmaModalVisible(false)} - prompt={personalKarmaStopDonationsPrompt} - confirmText={translate('common.disable')} - cancelText={translate('common.cancel')} - danger - /> ); } From 61d7ea883051f43a444eb4269ca1306fe9db0e8e Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Wed, 8 Apr 2026 18:41:49 +0100 Subject: [PATCH 18/18] feat: Integrate FullPageNotFoundView for enhanced user experience in AddPaymentCard component --- .../Subscription/PaymentCard/index.tsx | 100 +++++++++++------- 1 file changed, 62 insertions(+), 38 deletions(-) diff --git a/src/pages/settings/Subscription/PaymentCard/index.tsx b/src/pages/settings/Subscription/PaymentCard/index.tsx index 81a1e1273901..6d2bd12a1a4c 100644 --- a/src/pages/settings/Subscription/PaymentCard/index.tsx +++ b/src/pages/settings/Subscription/PaymentCard/index.tsx @@ -1,7 +1,9 @@ +import {useRoute} from '@react-navigation/native'; import {accountIDSelector} from '@selectors/Session'; -import React, {useCallback, useEffect} from 'react'; +import React, {useCallback, useEffect, useMemo} from 'react'; import {View} from 'react-native'; import PaymentCardForm from '@components/AddPaymentCard/PaymentCardForm'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; import type {FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -24,17 +26,21 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {getMCardNumberString, getMonthFromExpirationDateString, getYearFromExpirationDateString} from '@libs/CardUtils'; import {convertToShortDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; +import {getCardForSubscriptionBilling} from '@libs/SubscriptionUtils'; import CardAuthenticationModal from '@pages/settings/Subscription/CardAuthenticationModal'; import {addSubscriptionPaymentCard, clearPaymentCardFormErrorAndSubmit} from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; function AddPaymentCard() { + const route = useRoute(); const styles = useThemeStyles(); const {translate} = useLocalize(); const privateSubscription = usePrivateSubscription(); const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector}); + const [userBillingFundID] = useOnyx(ONYXKEYS.NVP_BILLING_FUND_ID); const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); const subscriptionPlan = useSubscriptionPlan(); @@ -45,6 +51,19 @@ function AddPaymentCard() { const isCollect = subscriptionPlan === CONST.POLICY.TYPE.TEAM; const isAnnual = privateSubscription?.type === CONST.SUBSCRIPTION.TYPE.ANNUAL; const {asset: ShieldYellow} = useMemoizedLazyAsset(() => loadIllustration('ShieldYellow' as IllustrationName)); + + const billingCard = useMemo(() => { + const userBillingCard = userBillingFundID ? fundList?.[`${userBillingFundID}`] : undefined; + if (userBillingCard?.accountData) { + return userBillingCard; + } + + return getCardForSubscriptionBilling(fundList); + }, [fundList, userBillingFundID]); + + const isSaveTheWorldAddPaymentCardRoute = route.name === SCREENS.SAVE_THE_WORLD.ADD_PAYMENT_CARD; + const shouldShowBlockingView = isSaveTheWorldAddPaymentCardRoute && !!billingCard; + const subscriptionPricingInfo = hasTeam2025Pricing && isCollect ? translate('subscription.yourPlan.pricePerMemberPerMonth', convertToShortDisplayString(subscriptionPrice, preferredCurrency)) @@ -90,43 +109,48 @@ function AddPaymentCard() { return ( - - - - {translate('subscription.paymentCard.enterPaymentCardDetails')}} - footerContent={ - <> -
( - - {translate('subscription.paymentCard.security')}{' '} - - {translate('subscription.paymentCard.learnMoreAboutSecurity')} - - - )} - /> - {subscriptionPricingInfo} - - } - /> - - - + + + + + {translate('subscription.paymentCard.enterPaymentCardDetails')}} + footerContent={ + <> +
( + + {translate('subscription.paymentCard.security')}{' '} + + {translate('subscription.paymentCard.learnMoreAboutSecurity')} + + + )} + /> + {subscriptionPricingInfo} + + } + /> + + + + ); }