diff --git a/assets/images/expensifyCard/cardProtectionIllustration.svg b/assets/images/expensifyCard/cardProtectionIllustration.svg new file mode 100644 index 0000000000000..fecea99297535 --- /dev/null +++ b/assets/images/expensifyCard/cardProtectionIllustration.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/CONST/index.ts b/src/CONST/index.ts index e13ba87a9aa90..61b7a166a3076 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -9174,6 +9174,7 @@ const CONST = { }, RULES: { INDIVIDUAL_EXPENSES_MENU_ITEM: 'WorkspaceRules-IndividualExpensesMenuItem', + SPEND_RULE_ITEM: 'WorkspaceRules-SpendRuleItem', MERCHANT_RULE_ITEM: 'WorkspaceRules-MerchantRuleItem', ADD_MERCHANT_RULE: 'WorkspaceRules-AddMerchantRule', MERCHANT_RULE_SECTION_ITEM: 'WorkspaceRules-MerchantRuleSectionItem', diff --git a/src/components/ConfirmContent.tsx b/src/components/ConfirmContent.tsx index 37a9ce0e4cd1e..e00a6024fe1a5 100644 --- a/src/components/ConfirmContent.tsx +++ b/src/components/ConfirmContent.tsx @@ -97,6 +97,9 @@ type ConfirmContentProps = { /** Styles for the image */ imageStyles?: StyleProp; + /** Whether to fit the image to the container */ + shouldFitImageToContainer?: boolean; + /** Whether the modal is visible */ isVisible: boolean; @@ -129,6 +132,7 @@ function ConfirmContent({ shouldShowDismissIcon = false, image, imageStyles, + shouldFitImageToContainer = false, titleContainerStyles, shouldReverseStackedButtons = false, isVisible, @@ -147,10 +151,11 @@ function ConfirmContent({ {!!image && ( diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx index 02be40520fb3d..d85baa8cd0fce 100755 --- a/src/components/ConfirmModal.tsx +++ b/src/components/ConfirmModal.tsx @@ -98,6 +98,9 @@ type ConfirmModalProps = { /** Styles for the image */ imageStyles?: StyleProp; + /** Whether to fit the image to the container */ + shouldFitImageToContainer?: boolean; + /** * Whether the modal should enable the new focus manager. * We are attempting to migrate to a new refocus manager, adding this property for gradual migration. @@ -115,6 +118,9 @@ type ConfirmModalProps = { /** Whether to ignore the back handler during transition */ shouldIgnoreBackHandlerDuringTransition?: boolean; + + /** Merged into the modal container after default confirm styles (e.g. `width` overrides `variables.sideBarWidth` on wide screens). */ + innerContainerStyle?: ViewStyle; }; function ConfirmModal({ @@ -140,6 +146,7 @@ function ConfirmModal({ onConfirm, image, imageStyles, + shouldFitImageToContainer = false, iconWidth, iconHeight, iconFill, @@ -152,6 +159,7 @@ function ConfirmModal({ isConfirmLoading, shouldHandleNavigationBack, shouldIgnoreBackHandlerDuringTransition, + innerContainerStyle, }: ConfirmModalProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use the correct modal type // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth @@ -174,7 +182,7 @@ function ConfirmModal({ shouldSetModalVisibility={shouldSetModalVisibility} onModalHide={onModalHide} type={isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM} - innerContainerStyle={styles.pv0} + innerContainerStyle={innerContainerStyle ? {...styles.pv0, ...innerContainerStyle} : styles.pv0} shouldEnableNewFocusManagement={shouldEnableNewFocusManagement} restoreFocusType={restoreFocusType} shouldHandleNavigationBack={shouldHandleNavigationBack} @@ -210,6 +218,7 @@ function ConfirmModal({ shouldReverseStackedButtons={shouldReverseStackedButtons} image={image} imageStyles={imageStyles} + shouldFitImageToContainer={shouldFitImageToContainer} isConfirmLoading={isConfirmLoading} /> diff --git a/src/components/Icon/chunks/illustrations.chunk.ts b/src/components/Icon/chunks/illustrations.chunk.ts index d8609ac99a577..fd232f9afa7d3 100644 --- a/src/components/Icon/chunks/illustrations.chunk.ts +++ b/src/components/Icon/chunks/illustrations.chunk.ts @@ -38,6 +38,7 @@ import MultiScan from '@assets/images/educational-illustration__multi-scan.svg'; import ExpensifyCardCoins from '@assets/images/emptystate__expensify-card-coins.svg'; import ExpensifyCardImage from '@assets/images/expensify-card.svg'; import ExpensifyCardIllustration from '@assets/images/expensifyCard/cardIllustration.svg'; +import ExpensifyCardProtectionIllustration from '@assets/images/expensifyCard/cardProtectionIllustration.svg'; // Other Images import Hand from '@assets/images/hand.svg'; import LaptopOnDeskWithCoffeeAndKey from '@assets/images/laptop-on-desk-with-coffee-and-key.svg'; @@ -242,6 +243,7 @@ const Illustrations = { // Expensify Card ExpensifyCardIllustration, + ExpensifyCardProtectionIllustration, // Product Illustrations Abracadabra, diff --git a/src/languages/de.ts b/src/languages/de.ts index f26613f584497..26e2627514146 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6721,6 +6721,24 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und title: 'Spesenrichtlinie', cardSubtitle: 'Hier ist die Spesenrichtlinie deines Teams hinterlegt, damit alle denselben Stand haben, was abgedeckt ist.', }, + spendRules: { + title: 'Ausgaben', + subtitle: 'Genehmigen oder lehnen Sie Expensify Karte-Transaktionen in Echtzeit ab.', + defaultRuleDescription: 'Alle Karten', + block: 'Blockieren', + defaultRuleTitle: 'Kategorien: Erotikdienstleistungen, Geldautomaten, Glücksspiele, Geldüberweisungen', + builtInProtectionModal: { + title: 'Expensify Karten bieten integrierten Schutz – jederzeit', + description: `Expensify lehnt diese Belastungen immer ab: + + • Erwachsenenservices + • Geldautomaten + • Glücksspiel + • Geldüberweisungen + +Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu schützen.`, + }, + }, }, planTypePage: { planTypes: { diff --git a/src/languages/en.ts b/src/languages/en.ts index a88b7dc2cd64e..fa5316a2c38d8 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6709,6 +6709,17 @@ const translations = { title: 'Expense policy', cardSubtitle: "Here's where your team's expense policy lives, so everyone's on the same page about what's covered.", }, + spendRules: { + title: 'Spend', + subtitle: 'Approve or decline Expensify Card transactions in realtime.', + defaultRuleDescription: 'All cards', + block: 'Block', + defaultRuleTitle: 'Categories: Adult services, ATMs, gambling, money transfers', + builtInProtectionModal: { + title: 'Expensify Cards offer built-in protection - always', + description: `Expensify always declines these charges:\n\n • Adult services\n • ATMs\n • Gambling\n • Money transfers\n\nAdd more spend rules to protect company cash flow.`, + }, + }, }, planTypePage: { planTypes: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 94ea3d6623bb5..cc39a3af505cd 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6620,6 +6620,17 @@ ${amount} para ${merchant} - ${date}`, title: 'Reglas personalizadas', cardSubtitle: 'Aquí es donde se definen las reglas de tu equipo, para que todos sepan lo que esta cubierto.', }, + spendRules: { + title: 'Gastos', + subtitle: 'Aprueba o rechaza transacciones de la tarjeta Expensify en tiempo real.', + defaultRuleDescription: 'Todas las tarjetas', + block: 'Bloquear', + defaultRuleTitle: 'Categorías: Servicios para adultos, cajeros automáticos, juegos de azar, transferencias de dinero', + builtInProtectionModal: { + title: 'Las tarjetas Expensify ofrecen protección integrada, siempre', + description: `Expensify siempre rechaza estos cargos:\n\n • Servicios para adultos\n • Cajeros automáticos\n • Juegos de azar\n • Transferencias de dinero\n\nAgregue más reglas de gasto para proteger el flujo de caja de la empresa.`, + }, + }, }, }, getAssistancePage: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 119438e3629f3..09adf914e4798 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -6744,6 +6744,24 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip title: 'Politique de dépenses', cardSubtitle: 'C’est ici que se trouve la politique de dépenses de votre équipe, pour que tout le monde soit d’accord sur ce qui est couvert.', }, + spendRules: { + title: 'Dépenser', + subtitle: 'Approuvez ou refusez les transactions Carte Expensify en temps réel.', + defaultRuleDescription: 'Toutes les cartes', + block: 'Bloquer', + defaultRuleTitle: 'Catégories : services pour adultes, DAB, jeux d’argent, transferts d’argent', + builtInProtectionModal: { + title: 'Les Cartes Expensify offrent une protection intégrée – en permanence', + description: `Expensify refuse toujours ces dépenses : + + • Services pour adultes + • DAB + • Jeux d’argent + • Transferts d’argent + +Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’entreprise.`, + }, + }, }, planTypePage: { planTypes: { diff --git a/src/languages/it.ts b/src/languages/it.ts index c9c8209d40639..8cdd659042172 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6710,6 +6710,24 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo title: 'Politica di spesa', cardSubtitle: 'Qui trovi il regolamento spese del tuo team, così tutti sono allineati su cosa è coperto.', }, + spendRules: { + title: 'Spesa', + subtitle: 'Approva o rifiuta le transazioni della Carta Expensify in tempo reale.', + defaultRuleDescription: 'Tutte le carte', + block: 'Blocca', + defaultRuleTitle: 'Categorie: servizi per adulti, sportelli bancomat, gioco d’azzardo, trasferimenti di denaro', + builtInProtectionModal: { + title: 'Le Carte Expensify offrono sempre una protezione integrata', + description: `Expensify rifiuta sempre questi addebiti: + + • Servizi per adulti + • Bancomat + • Gioco d’azzardo + • Trasferimenti di denaro + +Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, + }, + }, }, planTypePage: { planTypes: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 407a92f60fec7..cfa792e2a3625 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6636,6 +6636,24 @@ ${reportName} title: '経費ポリシー', cardSubtitle: 'ここはチームの経費ポリシーが保存されている場所です。何が対象になるか、全員が同じ認識を持てます。', }, + spendRules: { + title: '支出', + subtitle: 'Expensify カードの取引をリアルタイムで承認または却下できます。', + defaultRuleDescription: 'すべてのカード', + block: 'ブロック', + defaultRuleTitle: 'カテゴリ:アダルトサービス、ATM、ギャンブル、送金', + builtInProtectionModal: { + title: 'Expensify カードには、常に標準で保護機能があります', + description: `Expensify は、次のような支払いを常に拒否します: + + ・アダルトサービス + ・ATM + ・ギャンブル + ・送金 + +会社のキャッシュフローを守るために、支出ルールをさらに追加しましょう。`, + }, + }, }, planTypePage: { planTypes: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index dbe5744bc7fb5..22c8c3676532c 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6690,6 +6690,24 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar title: 'Declaratiebeleid', cardSubtitle: 'Hier staat het declaratiebeleid van je team, zodat iedereen hetzelfde beeld heeft van wat er wordt vergoed.', }, + spendRules: { + title: 'Uitgaven', + subtitle: 'Keur Expensify Kaart-transacties in realtime goed of af.', + defaultRuleDescription: 'Alle kaarten', + block: 'Blokkeren', + defaultRuleTitle: 'Categorieën: diensten voor volwassenen, geldautomaten, gokken, geldoverdrachten', + builtInProtectionModal: { + title: 'Expensify Kaarten bieden altijd ingebouwde bescherming', + description: `Expensify weigert deze uitgaven altijd: + + • Services voor volwassenen + • Geldautomaten (ATM's) + • Gokken + • Geldoverschrijvingen + +Voeg meer bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, + }, + }, }, planTypePage: { planTypes: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index d61887f0ed3e9..1e06c44a27d7a 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6683,6 +6683,24 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i title: 'Polityka wydatków', cardSubtitle: 'To tutaj znajduje się polityka wydatków Twojego zespołu, aby wszyscy mieli jasność co do tego, co jest objęte.', }, + spendRules: { + title: 'Wydatki', + subtitle: 'Zatwierdzaj lub odrzucaj transakcje Karty Expensify w czasie rzeczywistym.', + defaultRuleDescription: 'Wszystkie karty', + block: 'Zablokuj', + defaultRuleTitle: 'Kategorie: Usługi dla dorosłych, bankomaty, hazard, przelewy pieniężne', + builtInProtectionModal: { + title: 'Karty Expensify zapewniają wbudowaną ochronę – zawsze', + description: `Expensify zawsze odrzuca te obciążenia: + + • Usługi dla dorosłych + • Bankomaty + • Hazard + • Przelewy pieniężne + +Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, + }, + }, }, planTypePage: { planTypes: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index b335a965bae31..dd890d422914f 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6688,6 +6688,24 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e title: 'Política de despesas', cardSubtitle: 'Aqui é onde fica a política de despesas da sua equipe, para que todo mundo esteja alinhado sobre o que é coberto.', }, + spendRules: { + title: 'Gasto', + subtitle: 'Aprove ou recuse transações do Cartão Expensify em tempo real.', + defaultRuleDescription: 'Todos os cartões', + block: 'Bloquear', + defaultRuleTitle: 'Categorias: Serviços adultos, caixas eletrônicos, jogos de azar, transferências de dinheiro', + builtInProtectionModal: { + title: 'Os Cartões Expensify oferecem proteção integrada — sempre', + description: `A Expensify sempre recusa estas cobranças: + + • Serviços adultos + • Caixas eletrônicos (ATM) + • Jogos de azar + • Transferências de dinheiro + +Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, + }, + }, }, planTypePage: { planTypes: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 91512e9023c0e..14b1b754196fc 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6518,6 +6518,24 @@ ${reportName} title: '报销政策', cardSubtitle: '这是你们团队的报销政策所在之处,让所有人都清楚哪些内容在报销范围之内。', }, + spendRules: { + title: '支出', + subtitle: '实时批准或拒绝 Expensify 卡交易。', + defaultRuleDescription: '所有卡片', + block: '屏蔽', + defaultRuleTitle: '类别:成人服务、ATM、赌博、转账', + builtInProtectionModal: { + title: 'Expensify 卡始终提供内置保护', + description: `Expensify 始终会拒绝以下消费: + + • 成人服务 + • ATM + • 赌博 + • 转账 + +添加更多消费规则以保护公司现金流。`, + }, + }, }, planTypePage: { planTypes: { diff --git a/src/pages/workspace/rules/MerchantRulesSection.tsx b/src/pages/workspace/rules/MerchantRulesSection.tsx index daa7c99351736..4e993e03e624d 100644 --- a/src/pages/workspace/rules/MerchantRulesSection.tsx +++ b/src/pages/workspace/rules/MerchantRulesSection.tsx @@ -102,7 +102,7 @@ function MerchantRulesSection({policyID}: MerchantRulesSectionProps) { }, [codingRules]); const renderTitle = () => ( - + {translate('workspace.rules.merchantRules.title')} ; @@ -55,6 +56,7 @@ function PolicyRulesPage({route}: PolicyRulesPageProps) { + {!!policy?.areExpensifyCardsEnabled && } diff --git a/src/pages/workspace/rules/SpendRules/SpendRulesSection.tsx b/src/pages/workspace/rules/SpendRules/SpendRulesSection.tsx new file mode 100644 index 0000000000000..500835d3f1b14 --- /dev/null +++ b/src/pages/workspace/rules/SpendRules/SpendRulesSection.tsx @@ -0,0 +1,115 @@ +import React from 'react'; +import {View} from 'react-native'; +import Badge from '@components/Badge'; +import Icon from '@components/Icon'; +import MenuItem from '@components/MenuItem'; +import Section from '@components/Section'; +import Text from '@components/Text'; +import useConfirmModal from '@hooks/useConfirmModal'; +import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; + +function SpendRulesSection() { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const theme = useTheme(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['Lock']); + const {showConfirmModal} = useConfirmModal(); + const illustrations = useMemoizedLazyIllustrations(['ExpensifyCardProtectionIllustration']); + + const showBuiltInProtectionModal = () => { + showConfirmModal({ + image: illustrations.ExpensifyCardProtectionIllustration, + imageStyles: [styles.w100], + shouldFitImageToContainer: true, + title: translate('workspace.rules.spendRules.builtInProtectionModal.title'), + titleStyles: [styles.textHeadlineH1], + titleContainerStyles: [styles.mb3], + prompt: translate('workspace.rules.spendRules.builtInProtectionModal.description'), + promptStyles: [styles.mb1], + shouldShowCancelButton: false, + success: false, + confirmText: translate('common.buttonConfirm'), + innerContainerStyle: shouldUseNarrowLayout ? undefined : StyleUtils.getWidthStyle(variables.builtInProtectionModalWidth), + }); + }; + + const defaultRuleTitle = translate('workspace.rules.spendRules.defaultRuleTitle'); + const descriptionLabel = translate('workspace.rules.spendRules.defaultRuleDescription'); + const blockLabel = translate('workspace.rules.spendRules.block'); + + const renderSectionTitle = () => ( + + {translate('workspace.rules.spendRules.title')} + + + ); + + const menuItemBody = ( + + + + + {defaultRuleTitle} + + + + + + {descriptionLabel} + + + + ); + + return ( +
+ +
+ ); +} + +SpendRulesSection.displayName = 'SpendRulesSection'; + +export default SpendRulesSection; diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 1f8452a1d0afe..654ca4af47058 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -254,6 +254,7 @@ export default { onboardingModalWidth: 640, holdEducationModalWidth: 400, changePolicyEducationModalWidth: 400, + builtInProtectionModalWidth: 400, changePolicyEducationModalIconWidth: 147.69, changePolicyEducationModalIconHeight: 180, transactionReceiptButtonWidth: 100,