Skip to content

[Home Page] [Release 2] Create TimeSensitiveSection Component#80951

Merged
mountiny merged 9 commits intoExpensify:mainfrom
software-mansion-labs:home-page/time-sensitive-section-v1
Jan 30, 2026
Merged

[Home Page] [Release 2] Create TimeSensitiveSection Component#80951
mountiny merged 9 commits intoExpensify:mainfrom
software-mansion-labs:home-page/time-sensitive-section-v1

Conversation

@adamgrzybowski
Copy link
Contributor

@adamgrzybowski adamgrzybowski commented Jan 29, 2026

Explanation of Change

This PR implements a new "Time Sensitive" section on the Home page that displays promotional discount offers for users during their free trial period.

Changes

  • Created TimeSensitiveSection component with two offer items:
    • 50% Off Offer: Shown within the first 24 hours of trial with a live countdown timer
    • 25% Off Offer: Shown on days 2-7 of trial when no billing card is added
  • Added formatCountdownTimer helper to DateUtils for formatting countdown timers (uses existing translation abbreviations for h/m/s)
  • Added iconFill prop to WidgetContainer for custom icon colors
  • Added common.secondAbbreviation translation
  • Refactored widgets to handle their own visibility internally (may return null)

Business Logic

  • Offers are mutually exclusive (only one shows at a time based on trial day)
  • Offers only show for Control plan users (not Team plan with 2025 pricing)
  • 25% offer requires no billing card to be added
  • Both CTAs navigate to subscription settings

Fixed Issues

$ #79982
PROPOSAL:

Tests

  • Verify that no errors appear in the JS console

Offline tests

QA Steps

// TODO: These must be filled out, or the issue title must include "[No QA]."

  • Verify that no errors appear in the JS console

Test 50% Off Offer

  1. Set up a user whose trial started today (within last 24 hours) AND has the control plan active AND has no billing card
  2. Navigate to Home page
  3. Verify: "Time sensitive" section appears with red stopwatch icon and red title
  4. Verify: Item shows treasure chest icon, "Get 50% off your first year!" title
  5. Verify: Subtitle shows countdown in format "XXh XXm XXs remaining" updating every second
  6. Verify: "Claim" button is visible
  7. Tap "Claim" → Verify: Navigates to subscription settings

Test 25% Off Offer

  1. Set up a user whose trial started 2-7 days ago AND has no billing card AND has the control plan active
  2. Navigate to Home page
  3. Verify: "Time sensitive" section appears
  4. Verify: Item shows treasure chest icon, "Get 25% off your first year!" title
  5. Verify: Subtitle shows "X days remaining" (correct pluralization)
  6. Tap "Claim" → Verify: Navigates to subscription settings

Test Section Hidden

  1. Verify: Section hidden when not on free trial
  2. Verify: Section hidden when trial ended (day 8+)
  3. Verify: Section hidden when the control plan is NOT active
  4. Verify: 25% offer hidden when billing card exists

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified there are no new alerts related to the canBeMissing param for useOnyx
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-01-29.at.12.57.58.mov
Screen.Recording.2026-01-29.at.12.58.58.mov

@melvin-bot
Copy link

melvin-bot bot commented Jan 29, 2026

Hey, I noticed you changed src/languages/en.ts in a PR from a fork. For security reasons, translations are not generated automatically for PRs from forks.

If you want to automatically generate translations for other locales, an Expensify employee will have to:

  1. Look at the code and make sure there are no malicious changes.
  2. Run the Generate static translations GitHub workflow. If you have write access and the K2 extension, you can simply click: [this button]

Alternatively, if you are an external contributor, you can run the translation script locally with your own OpenAI API key. To learn more, try running:

npx ts-node ./scripts/generateTranslations.ts --help

Typically, you'd want to translate only what you changed by running npx ts-node ./scripts/generateTranslations.ts --compare-ref main

@adamgrzybowski adamgrzybowski marked this pull request as ready for review January 29, 2026 21:14
@adamgrzybowski adamgrzybowski requested review from a team as code owners January 29, 2026 21:14
@melvin-bot melvin-bot bot requested review from JmillsExpensify and ZhenjaHorbach and removed request for a team January 29, 2026 21:14
@melvin-bot
Copy link

melvin-bot bot commented Jan 29, 2026

@ZhenjaHorbach Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot melvin-bot bot removed the request for review from a team January 29, 2026 21:14
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2cf3edfebd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +29 to +36
const discountInfo = getEarlyDiscountInfo(firstDayFreeTrial);

// Don't show offers for Team plan with 2025 pricing
const isTeamWithNew2025Pricing = hasTeam2025Pricing && subscriptionPlan === CONST.POLICY.TYPE.TEAM;

// Determine which offer to show (they are mutually exclusive)
const shouldShow50off = discountInfo?.discountType === 50 && !isTeamWithNew2025Pricing;
const shouldShow25off = discountInfo?.discountType === 25 && !doesUserHavePaymentCardAdded(userBillingFundID) && !isTeamWithNew2025Pricing;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute offer visibility as time advances

Because discountInfo is computed once during the parent render and never updated, the widget won’t respond to time-based changes while the user remains on Home. If the first 24 hours elapse (or the trial window ends) without another parent re-render, Offer50off will return null but TimeSensitiveSection will still render the container (and won’t switch to 25% or hide entirely), leaving an empty “Time sensitive” section. This can happen for users who keep the app open across the 24‑hour boundary. Consider moving the timer state up or recomputing discountInfo on an interval so the parent visibility logic updates.

Useful? React with 👍 / 👎.

@codecov
Copy link

codecov bot commented Jan 29, 2026

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/CONST/index.ts 87.50% <ø> (ø)
src/libs/DateUtils.ts 64.30% <100.00%> (+0.13%) ⬆️
src/styles/index.ts 41.79% <ø> (-0.68%) ⬇️
src/pages/home/HomePage.tsx 0.00% <0.00%> (ø)
src/components/WidgetContainer.tsx 0.00% <0.00%> (ø)
...ges/home/TimeSensitiveSection/items/Offer25off.tsx 0.00% <0.00%> (ø)
...ges/home/TimeSensitiveSection/items/Offer50off.tsx 0.00% <0.00%> (ø)
src/pages/home/TimeSensitiveSection/index.tsx 0.00% <0.00%> (ø)
... and 41 files with indirect coverage changes

@github-actions
Copy link
Contributor

🚧 @grgia has triggered a test Expensify/App build. You can view the workflow run here.

@github-actions

This comment has been minimized.

@mountiny
Copy link
Contributor

@WojtekBoman TS failing on this one

@ZhenjaHorbach
Copy link
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified there are no new alerts related to the canBeMissing param for useOnyx
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
2026-01-30.00.07.28.mov
Android: mWeb Chrome
2026-01-30.00.09.41.mov
iOS: HybridApp
2026-01-30.00.07.28.mov
iOS: mWeb Safari
2026-01-30.00.09.41.mov
MacOS: Chrome / Safari
2026-01-30.00.04.06.mov

@OSBotify
Copy link
Contributor

🦜 Polyglot Parrot! 🦜

Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues:

View the translation diff
diff --git a/src/languages/de.ts b/src/languages/de.ts
index b19bc081..cc241c33 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -642,6 +642,7 @@ const translations: TranslationDeepObject<typeof en> = {
         month: 'Monat',
         home: 'Startseite',
         week: 'Woche',
+        secondAbbreviation: 's',
     },
     supportalNoAccess: {
         title: 'Nicht so schnell',
@@ -8265,6 +8266,12 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`,
                 upcomingTodos: 'Anstehende To-dos werden hier angezeigt.',
             },
         },
+        timeSensitiveSection: {
+            title: 'Zeitkritisch',
+            cta: 'Antrag',
+            offer50off: {title: 'Erhalte 50 % Rabatt auf dein erstes Jahr!', subtitle: ({formattedTime}: {formattedTime: string}) => `${formattedTime} verbleibend`},
+            offer25off: {title: 'Erhalten Sie 25 % Rabatt auf Ihr erstes Jahr!', subtitle: ({days}: {days: number}) => `${days} ${days === 1 ? 'Tag' : 'Tage'} verbleiben`},
+        },
     },
 };
 // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 07c89ef1..7e65bb26 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -644,6 +644,7 @@ const translations: TranslationDeepObject<typeof en> = {
         month: 'Mois',
         home: 'Accueil',
         week: 'Semaine',
+        secondAbbreviation: 's',
     },
     supportalNoAccess: {
         title: 'Pas si vite',
@@ -7036,7 +7037,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
                 [CONST.SEARCH.GROUP_BY.CARD]: 'Carte',
                 [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'ID de retrait',
                 [CONST.SEARCH.GROUP_BY.CATEGORY]: 'Catégorie',
-                [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Commerçant',
+                [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Marchand',
                 [CONST.SEARCH.GROUP_BY.TAG]: 'Étiquette',
                 [CONST.SEARCH.GROUP_BY.MONTH]: 'Mois',
                 [CONST.SEARCH.GROUP_BY.WEEK]: 'Semaine',
@@ -8271,6 +8272,12 @@ Voici un *reçu test* pour vous montrer comment cela fonctionne :`,
                 upcomingTodos: 'Les tâches à venir apparaîtront ici.',
             },
         },
+        timeSensitiveSection: {
+            title: 'Urgent',
+            cta: 'Demande',
+            offer50off: {title: 'Obtenez 50 % de réduction sur votre première année !', subtitle: ({formattedTime}: {formattedTime: string}) => `${formattedTime} restant`},
+            offer25off: {title: 'Obtenez 25 % de réduction sur votre première année !', subtitle: ({days}: {days: number}) => `${days} ${days === 1 ? 'jour' : 'jours'} restants`},
+        },
     },
 };
 // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/it.ts b/src/languages/it.ts
index d3f3eaca..7676b1e8 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -643,6 +643,7 @@ const translations: TranslationDeepObject<typeof en> = {
         month: 'Mese',
         home: 'Home',
         week: 'Settimana',
+        secondAbbreviation: 's',
     },
     supportalNoAccess: {
         title: 'Non così in fretta',
@@ -7014,7 +7015,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
                 [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'ID prelievo', //_/\__/_/  \_,_/\__/\__/\_,_/
                 [CONST.SEARCH.GROUP_BY.CATEGORY]: 'Categoria',
                 [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Esercente',
-                [CONST.SEARCH.GROUP_BY.TAG]: 'Tag',
+                [CONST.SEARCH.GROUP_BY.TAG]: 'Etichetta',
                 [CONST.SEARCH.GROUP_BY.MONTH]: 'Mese',
                 [CONST.SEARCH.GROUP_BY.WEEK]: 'Settimana',
             },
@@ -8251,6 +8252,12 @@ Ecco una *ricevuta di prova* per mostrarti come funziona:`,
                 upcomingTodos: 'Le prossime attività da fare verranno visualizzate qui.',
             },
         },
+        timeSensitiveSection: {
+            title: 'Urgente',
+            cta: 'Richiesta',
+            offer50off: {title: 'Ottieni il 50% di sconto sul tuo primo anno!', subtitle: ({formattedTime}: {formattedTime: string}) => `${formattedTime} rimanenti`},
+            offer25off: {title: 'Ottieni il 25% di sconto sul tuo primo anno!', subtitle: ({days}: {days: number}) => `${days} ${days === 1 ? 'giorno' : 'giorni'} rimanenti`},
+        },
     },
 };
 // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 188d04a7..8bf725bf 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -642,6 +642,7 @@ const translations: TranslationDeepObject<typeof en> = {
         month: '月',
         home: 'ホーム',
         week: '週',
+        secondAbbreviation: '秒',
     },
     supportalNoAccess: {
         title: 'ちょっと待ってください',
@@ -8163,6 +8164,12 @@ Expensify の使い方をお見せするための*テストレシート*がこ
                 upcomingTodos: '今後のTo-doがここに表示されます。',
             },
         },
+        timeSensitiveSection: {
+            title: '至急',
+            cta: '申請',
+            offer50off: {title: '初年度が50%オフ!', subtitle: ({formattedTime}: {formattedTime: string}) => `残り${formattedTime}`},
+            offer25off: {title: '初年度が25%オフ!', subtitle: ({days}: {days: number}) => `残り ${days} ${days === 1 ? '日' : '日'}`},
+        },
     },
 };
 // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index f1ff6c99..7e92a6ea 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -643,6 +643,7 @@ const translations: TranslationDeepObject<typeof en> = {
         month: 'Maand',
         home: 'Start',
         week: 'Week',
+        secondAbbreviation: 's',
     },
     supportalNoAccess: {
         title: 'Niet zo snel',
@@ -6997,7 +6998,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten
                 [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'Opname-ID',
                 [CONST.SEARCH.GROUP_BY.CATEGORY]: 'Categorie',
                 [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Handelaar',
-                [CONST.SEARCH.GROUP_BY.TAG]: 'Tag',
+                [CONST.SEARCH.GROUP_BY.TAG]: 'Label',
                 [CONST.SEARCH.GROUP_BY.MONTH]: 'Maand',
                 [CONST.SEARCH.GROUP_BY.WEEK]: 'Week',
             },
@@ -8227,6 +8228,12 @@ Hier is een *testbon* om je te laten zien hoe het werkt:`,
                 upcomingTodos: 'Aankomende taken verschijnen hier.',
             },
         },
+        timeSensitiveSection: {
+            title: 'Tijdgevoelig',
+            cta: 'Declaratie',
+            offer50off: {title: 'Krijg 50% korting op je eerste jaar!', subtitle: ({formattedTime}: {formattedTime: string}) => `${formattedTime} resterend`},
+            offer25off: {title: 'Krijg 25% korting op je eerste jaar!', subtitle: ({days}: {days: number}) => `Nog ${days} ${days === 1 ? 'dag' : 'dagen'} resterend`},
+        },
     },
 };
 // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 554e9d2a..e1626ebb 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -643,6 +643,7 @@ const translations: TranslationDeepObject<typeof en> = {
         month: 'Miesiąc',
         home: 'Strona główna',
         week: 'Tydzień',
+        secondAbbreviation: 's',
     },
     supportalNoAccess: {
         title: 'Nie tak szybko',
@@ -6985,7 +6986,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i
                 [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'ID wypłaty',
                 [CONST.SEARCH.GROUP_BY.CATEGORY]: 'Kategoria',
                 [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Sprzedawca',
-                [CONST.SEARCH.GROUP_BY.TAG]: 'Tag',
+                [CONST.SEARCH.GROUP_BY.TAG]: 'Znacznik',
                 [CONST.SEARCH.GROUP_BY.MONTH]: 'Miesiąc',
                 [CONST.SEARCH.GROUP_BY.WEEK]: 'Tydzień',
             },
@@ -8212,6 +8213,12 @@ Oto *paragon testowy*, który pokazuje, jak to działa:`,
                 upcomingTodos: 'Nadchodzące zadania do wykonania pojawią się tutaj.',
             },
         },
+        timeSensitiveSection: {
+            title: 'Pilne',
+            cta: 'Roszczenie',
+            offer50off: {title: 'Uzyskaj 50% zniżki na pierwszy rok!', subtitle: ({formattedTime}: {formattedTime: string}) => `Pozostało: ${formattedTime}`},
+            offer25off: {title: 'Uzyskaj 25% zniżki na pierwszy rok!', subtitle: ({days}: {days: number}) => `Pozostało ${days} ${days === 1 ? 'dzień' : 'dni'}`},
+        },
     },
 };
 // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index a558a4a3..3273d43a 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -642,6 +642,7 @@ const translations: TranslationDeepObject<typeof en> = {
         month: 'Mês',
         home: 'Início',
         week: 'Semana',
+        secondAbbreviation: 's',
     },
     supportalNoAccess: {
         title: 'Não tão rápido',
@@ -8221,6 +8222,12 @@ Aqui está um *recibo de teste* para mostrar como funciona:`,
                 upcomingTodos: 'Próximas tarefas aparecerão aqui.',
             },
         },
+        timeSensitiveSection: {
+            title: 'Urgente',
+            cta: 'Solicitação',
+            offer50off: {title: 'Ganhe 50% de desconto no seu primeiro ano!', subtitle: ({formattedTime}: {formattedTime: string}) => `${formattedTime} restante`},
+            offer25off: {title: 'Ganhe 25% de desconto no seu primeiro ano!', subtitle: ({days}: {days: number}) => `${days} ${days === 1 ? 'dia' : 'dias'} restantes`},
+        },
     },
 };
 // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index b9c9b59b..916c4fcc 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -639,6 +639,7 @@ const translations: TranslationDeepObject<typeof en> = {
         month: '月',
         home: '首页',
         week: '周',
+        secondAbbreviation: 's',
     },
     supportalNoAccess: {
         title: '先别急',
@@ -6829,10 +6830,11 @@ ${reportName}
             purchaseCurrency: '购买货币',
             groupBy: {
                 [CONST.SEARCH.GROUP_BY.FROM]: '来自',
-                [CONST.SEARCH.GROUP_BY.CARD]: '卡',
-                [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: '提现编号',
+                [CONST.SEARCH.GROUP_BY.CARD]: '卡片',
+                //_/\__/_/  \_,_/\__/\__/\_,_/
+                [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: '提现 ID',
                 [CONST.SEARCH.GROUP_BY.CATEGORY]: '类别',
-                [CONST.SEARCH.GROUP_BY.MERCHANT]: '商户',
+                [CONST.SEARCH.GROUP_BY.MERCHANT]: '商家',
                 [CONST.SEARCH.GROUP_BY.TAG]: '标签',
                 [CONST.SEARCH.GROUP_BY.MONTH]: '月',
                 [CONST.SEARCH.GROUP_BY.WEEK]: '周',
@@ -7981,6 +7983,12 @@ ${reportName}
             begin: '开始',
             emptyStateMessages: {nicelyDone: '做得很好', keepAnEyeOut: '敬请关注接下来的更新!', allCaughtUp: '你已经全部看完了', upcomingTodos: '即将进行的待办事项会显示在此处。'},
         },
+        timeSensitiveSection: {
+            title: '时间敏感',
+            cta: '报销申请',
+            offer50off: {title: '首年立享五折优惠!', subtitle: ({formattedTime}: {formattedTime: string}) => `剩余 ${formattedTime}`},
+            offer25off: {title: '首次年度订阅立享 25% 折扣!', subtitle: ({days}: {days: number}) => `剩余 ${days} ${days === 1 ? '天' : '天'}`},
+        },
     },
 };
 // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,

Note

You can apply these changes to your branch by copying the patch to your clipboard, then running pbpaste | git apply 😉

Comment on lines +31 to +36
// Don't show offers for Team plan with 2025 pricing
const isTeamWithNew2025Pricing = hasTeam2025Pricing && subscriptionPlan === CONST.POLICY.TYPE.TEAM;

// Determine which offer to show (they are mutually exclusive)
const shouldShow50off = discountInfo?.discountType === 50 && !isTeamWithNew2025Pricing;
const shouldShow25off = discountInfo?.discountType === 25 && !doesUserHavePaymentCardAdded(userBillingFundID) && !isTeamWithNew2025Pricing;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this logic is also in the Subscriptions page and it is same, right? this should be taken out to util method and covered with unit tests, can you do that please?

@adamgrzybowski adamgrzybowski requested a review from a team as a code owner January 29, 2026 23:28
@dubielzyk-expensify
Copy link
Contributor

Looking good to me. I see no Apple and Google logins

CleanShot 2026-01-29 at 15 44 34@2x

@mountiny
Copy link
Contributor

I see no Apple and Google logins

That is on all ad hocs

@mountiny
Copy link
Contributor

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f82b6f7f38

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +32 to +40
const discountInfo = getEarlyDiscountInfo(firstDayFreeTrial);

if (!shouldShowDiscount || !discountInfo) {
return null;
}

// Determine which offer to show based on discount type (they are mutually exclusive)
const shouldShow50off = discountInfo.discountType === 50;
const shouldShow25off = discountInfo.discountType === 25;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute discount type so offers switch after 24h

Because discountInfo is computed once per render and shouldShow50off/shouldShow25off are derived from it, the parent never re-evaluates which offer to show as time passes. If a user opens Home during the first 24 hours and keeps it open past the cutoff, Offer50off keeps rendering (its own interval only updates the countdown text) and the 25% offer never appears. This deviates from the “50% only within first 24h” logic; consider moving the timer/discountInfo state to the section or switching on discountInfo.discountType inside the 50% item so it unmounts once the first day elapses.

Useful? React with 👍 / 👎.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is edge case we do not have to worry about

Comment on lines +30 to +35
// Use the same logic as the subscription page to determine if discount banner should be shown
const shouldShowDiscount = shouldShowDiscountBanner(hasTeam2025Pricing, subscriptionPlan, firstDayFreeTrial, lastDayFreeTrial, userBillingFundID);
const discountInfo = getEarlyDiscountInfo(firstDayFreeTrial);

if (!shouldShowDiscount || !discountInfo) {
return null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don’t gate the 50% offer on missing billing card

shouldShowDiscountBanner returns false when userBillingFundID is set (see doesUserHavePaymentCardAdded in SubscriptionUtils), so this section never renders for users who already added a billing card. The PR description says only the 25% offer requires “no billing card,” so users who add a card within the first 24 hours would incorrectly miss the 50% offer. Consider using a visibility check that only applies the “no card” requirement to the 25% path.

Useful? React with 👍 / 👎.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The desceription will be udpated

Copy link
Contributor

@mountiny mountiny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, lets add unit test for it

import ROUTES from '@src/ROUTES';

type Offer25offProps = {
days: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc

import ROUTES from '@src/ROUTES';

type Offer50offProps = {
firstDayFreeTrial: string | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc

@github-actions
Copy link
Contributor

🚧 @mountiny has triggered a test Expensify/App build. You can view the workflow run here.

@mountiny mountiny changed the title [Home Page] Create TimeSensitiveSection Component [Home Page] [Release 21] Create TimeSensitiveSection Component Jan 30, 2026
@mountiny mountiny changed the title [Home Page] [Release 21] Create TimeSensitiveSection Component [Home Page] [Release 2] Create TimeSensitiveSection Component Jan 30, 2026
@github-actions
Copy link
Contributor

import ROUTES from '@src/ROUTES';

type Offer50offProps = {
firstDayFreeTrial: string | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc

import ROUTES from '@src/ROUTES';

type Offer25offProps = {
days: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc

@mountiny mountiny merged commit 8513b72 into Expensify:main Jan 30, 2026
35 checks passed
@OSBotify
Copy link
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify
Copy link
Contributor

🚀 Deployed to staging by https://github.com/mountiny in version: 9.3.11-16 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@nlemma
Copy link

nlemma commented Feb 2, 2026

@adamgrzybowski is there a shortcut we could use to get around waiting 8+ days for the last test case?

@adamgrzybowski
Copy link
Contributor Author

@nlemma, I think you can use an older account

@OSBotify
Copy link
Contributor

OSBotify commented Feb 5, 2026

🚀 Deployed to production by https://github.com/Julesssss in version: 9.3.12-1 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 failure ❌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants