Skip to content

Show the subscription expiring soon banner before the trial banners - #102307

Merged
amyevans merged 4 commits into
mainfrom
claude-subscriptionExpiringSoonBannerPrecedence
Sep 28, 2026
Merged

amyevans merged 4 commits into
mainfrom
claude-subscriptionExpiringSoonBannerPrecedence

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

Follow-up to Alert subscription owners when their annual subscription is about to expire with auto-renew off.

On Account > Subscription, the card section shows only the first banner that matches. The expiring-soon banner was checked last, after four trial banners. The pre-trial check (shouldShowPreTrialBillingBanner) also returns true when the free trial NVPs are absent. So an annual subscriber without those NVPs saw Start a free trial instead of Your subscription ends …, while Home and the green dot showed the warning correctly.

This PR checks shouldShowSubscriptionExpiringSoonUI first. It now matches the order the green dot already uses. The billing status override below the chain is unchanged.

getFreeTrialText now also returns nothing when the expiring-soon warning applies. So the Account menu, LHN, and report header no longer show a free trial badge next to the expiring-soon banner.

I didn't add a firstDayFreeTrial guard to shouldShowPreTrialBillingBanner. The help site says the trial starts once you start using your workspace. So a new owner in the pre-trial window may have no trial NVPs yet. The guard could hide that banner and the matching Start a free trial badge from getFreeTrialText. Reordering can't affect new owners, because the expiring-soon check requires an annual subscription with auto-renew off and an end date inside one month.

AI Tests

  • New tests/ui/SubscriptionCardSectionBannerTest.tsx: passes. The expiring-soon case fails without the fix.
  • tests/unit/SubscriptionUtilsTest.ts (including a new getFreeTrialText expiring-soon case), tests/unit/CardsSectionUtilsTest.ts: pass.
  • npm run typecheck, npm run lint, npm run fmt (no diff): pass.
  • react-compiler-compliance-check check on CardSection.tsx, FreeTrial.tsx, and useInitialSettingsPageMenuData.ts: pass. check-changed couldn't run locally because the base ref resolved to an empty value.

Fixed Issues

$ #101724
PROPOSAL: #101741 (comment)

Tests

Precondition:

  • You can test this on Web only
  1. Open the App
  2. Login with a new account
  3. Create a new workspace
  4. Go to Account > Subscription
  5. Open the browser console
  6. Run Onyx.merge('nvp_private_firstDayFreeTrial', null) and Onyx.merge('nvp_private_lastDayFreeTrial', null)
  7. Run Onyx.merge('nvp_private_subscription', {type: 'yearly2018', autoRenew: false, endDate: '2026-10-22'})
  8. Verify the banner says "your subscription ends ..."
  9. In LHN verify the Subscription row has a green dot and no free-trial label
  • Verify that no errors appear in the JS console

Offline tests

Same as Tests step

QA Steps

Same as Tests step

  • Verify that no errors appear in the JS console

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 followed proper code patterns (see Reviewing the code)
    • 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 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)
  • 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)
  • 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.
  • 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

The pre-trial check also passes when the free trial NVPs are absent, so an annual subscriber without them saw "Start a free trial" instead of the expiry warning on the Subscription page.

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team September 25, 2026 14:09
@melvin-bot melvin-bot Bot added Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web labels Sep 25, 2026
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I created this PR from an upstream branch since I don't have push access to your fork.

To take ownership of this branch and be able to push updates, run:

git remote add upstream https://github.com/Expensify/App.git
git fetch upstream claude-subscriptionExpiringSoonBannerPrecedence
git checkout -b claude-subscriptionExpiringSoonBannerPrecedence upstream/claude-subscriptionExpiringSoonBannerPrecedence
git push -u origin claude-subscriptionExpiringSoonBannerPrecedence

Then you can close this PR and open a new one from your fork.

@MelvinBot

MelvinBot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Android test steps verification results: ⚠️ partial

This PR's Tests section is still a TODO, so these steps come from the PR description and the parent PR's precondition (subscription state set with Onyx.merge). The expiring-soon banner correctly takes priority over the free-trial banner on Home and Account > Subscription, and it reverts when auto-renew is turned back on. Manage opens Subscription settings, but the Android screen has no auto-renew toggle by design, so step 4 fails for a reason unrelated to this PR.
Banner reverts to Start a free trial after autoRenew true

Step Status
Home shows a time-sensitive item about the subscription ending / turning on auto-renewThe Time sensitive section shows "Subscription ends Oct 10, 2026 / Turn on auto-renew to keep your current pricing" with a Manage button.
Home time-sensitive subscription item
✅
Account > Subscription menu row shows a green dotThe Subscription row shows a green dot. The "Start a free trial" badge next to it is separate menu-row text, not the page banner.
Subscription menu row with green dot
✅
Subscription page shows "Your subscription ends Oct 10, 2026" with Manage, not "Start a free trial"The page refetches real data when it opens, so it first showed the free-trial banner. After the subscription state was set again, the card section showed "Your subscription ends Oct 10, 2026 / Turn on auto-renew to keep your current pricing." with a Manage button and no free-trial banner.
Before re-merge: free-trial banner from fetched data
Expiring-soon banner shown
✅
Tapping Manage opens subscription settings with the auto-renew toggleManage opens Subscription settings. On mobile, that screen (SubscriptionSettings/index.native.tsx) is read-only and says "You can't make changes to your subscription in the mobile app." The auto-renew toggle exists only on web and desktop. This limitation already existed and is unrelated to this PR's banner-order change.
Android subscription settings without auto-renew toggle
❌
Regression: turning auto-renew back on removes the expiring-soon banner and restores the prior bannerAfter autoRenew: true was set, the banner reverted to "Start a free trial / As a next step, complete your setup checklist..." and the expiring-soon banner was gone.
Banner reverts to Start a free trial after autoRenew true
✅
No JS console errors related to the Subscription pageNo errors appeared while opening Subscription, tapping Manage, or turning auto-renew back on. The only warnings were generic ones (getSnapshot caching, unhandled DISMISS_MODAL, navigator state restoration), and they all fired before the Subscription page opened.
✅

view run · view recording

@MelvinBot

MelvinBot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Web test steps verification results: ⚠️ partial

This PR's Tests section is still a TODO, so these steps come from the PR description. On web, I couldn't set an expiring annual subscription because the web test tooling can't run Onyx.merge in the console. That blocks the expiring-soon checks, but the Android run covered them. The new-owner regression path passed on dev NewDot web: a new workspace owner still sees only the free-trial banner.
Home with free trial item, no subscription-ends item

Step Status
Expiring-soon banner shows ahead of "Start a free trial" (annual subscription, auto-renew off, ends within a month)Blocked. Setting this state needs Onyx.merge('nvp_private_subscription', …) in the browser console, and the web test tooling doesn't support page scripting. The same check passed on Android.
❌
New owner: after onboarding, the account owns a workspace and Account > Subscription appearsOnboarding created a workspace owned by the test account. The Account list shows Subscription with a "30 days left!" badge.
Account settings with Subscription row
✅
New owner: Subscription page shows the trial banner, not "Your subscription ends …"The banner reads "Trial: 30 days left! Add a payment card to continue using all of your favorite features." No "Your subscription ends" banner appears.
Subscription page with trial banner and subscription settings
✅
New owner: Home has no "Subscription ends … / Turn on auto-renew" itemHome shows "Free trial: 30 days left! Add a payment card" with an Add card button. There is no subscription-ends item.
Home with free trial item, no subscription-ends item
✅
New owner: Subscription settings render without errorsSubscription settings show "Pay-per-use, size none, Auto renew: on, Auto increase annual seats: off" with an Edit subscription button and no error state.
Subscription page with trial banner and subscription settings
✅
No JS console errors related to the Subscription pageThe web test tooling can't read console logs. Instead, the network log for Subscription and Home showed only 200 responses and no failed requests.
⚠️

view run · view recording

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot

Bug: The subscription page is showing the expiry banner, but the Account menu still uses the trial dates.

this happened after run this in browser

Onyx.merge('nvp_private_subscription', {
    type: 'yearly2018',
    autoRenew: false,
    endDate: '2026-10-22',
});
Screenshot 2026-09-25 at 9 09 20 PM

… shows

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Fixed in 874b607. Now, when the expiring-soon warning applies, the Account menu no longer shows the "30 days left!" trial badge.

Cause: The badge comes from getFreeTrialText, which only looked at the trial dates. It never checked the subscription state.

Fix: getFreeTrialText now takes privateSubscription and returns nothing when shouldShowSubscriptionExpiringSoonUI is true. This also covers the FreeTrial badge in the LHN and report header. I added a unit test for this case. Jest, typecheck, lint, and the React Compiler check pass.

One thing I didn't change: the Home free trial widget (useFreeTrial.ts:45) also uses only the trial dates. So with your Onyx.merge setup, it may still show next to the expiring warning. Let me know if you want it hidden too.


view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

The failing checks are unrelated to this PR, so I didn't push a fix. main has the same failures, and none of the failing files are in this PR's diff.

Failures:

Next step: Merge main into this branch and re-run the checks after someone fixes these tests on main.


view run

…piringSoonBannerPrecedence

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I merged main into this branch in 1bf4e01. That should fix the failing typecheck and test checks.

The failures came from a merge race on main, not from this PR. [No QA] Fix tests broken by merge race with getReportTransactions refactor has since fixed them on main. It updates ReportTest.ts, useSearchBulkActionsDownloadPDFTest.ts, and useSearchBulkActionsPayTest.ts.

The merge had no conflicts. I couldn't run the tests locally, so the new CI run will confirm the fix. Check independent approval will keep failing until a reviewer approves.


view run

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot Please update

Tests

Precondition:

  • You can test this on Web only
  1. Open the App
  2. Login with a new account
  3. Create a new workspace
  4. Go to Account > Subscription
  5. Open the browser console
  6. Run Onyx.merge('nvp_private_firstDayFreeTrial', null) and Onyx.merge('nvp_private_lastDayFreeTrial', null)
  7. Run Onyx.merge('nvp_private_subscription', {type: 'yearly2018', autoRenew: false, endDate: '2026-10-22'})
  8. Verify the banner says "your subscription ends ..."
  9. In LHN verify the Subscription row has a green dot and no free-trial label

Offline tests

Same as Tests step

QA Steps

Same as Tests step

PR Author Checklist

Mark all author checklist checked

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Updated the PR description:

  • Tests: replaced the TODO with your precondition and steps 1–9.
  • Offline tests and QA Steps: set to "Same as Tests step".
  • PR Author Checklist: checked every item.

I also added one line to the explanation for the getFreeTrialText badge fix from the last commit. I left the "Verify that no errors appear in the JS console" boxes under Tests and QA Steps unchecked, since those sit outside the author checklist.


view run

@ahmedGaber93

Copy link
Copy Markdown
Contributor
Screen.Recording.2026-09-25.at.10.34.59.PM.mov

@ahmedGaber93

Copy link
Copy Markdown
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 proper code patterns were followed (see Reviewing the code)
    • 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 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)
  • 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)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • 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
    • 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 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.
  • 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
Android: mWeb Chrome
iOS: HybridApp
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-09-25.at.10.34.59.PM.mov

@ahmedGaber93 ahmedGaber93 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM!

CC @Valforte since you review the orignal PR

@melvin-bot

melvin-bot Bot commented Sep 25, 2026

Copy link
Copy Markdown

We did not find an internal engineer to review this PR, trying to assign a random engineer to #101724 as well as to this PR... Please reach out for help on Slack if no one gets assigned!

@melvin-bot
melvin-bot Bot requested a review from amyevans September 25, 2026 19:52
@amyevans
amyevans marked this pull request as ready for review September 25, 2026 20:48
@amyevans
amyevans requested a review from a team as a code owner September 25, 2026 20:48
@melvin-bot
melvin-bot Bot requested review from ahmedGaber93 and garrettmknight and removed request for a team September 25, 2026 20:48
@melvin-bot

melvin-bot Bot commented Sep 25, 2026

Copy link
Copy Markdown

@ahmedGaber93 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 September 25, 2026 20:48
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T20:59:55.769118Z 1bf4e01 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Comment on lines +86 to +102
it('still shows the pre-trial banner when the subscription is not about to expire', async () => {
// Given an annual subscriber whose subscription still auto-renews, and no free trial NVPs
await Onyx.merge(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION, {
type: CONST.SUBSCRIPTION.TYPE.ANNUAL,
autoRenew: true,
endDate: format(addDays(new Date(), 10), CONST.DATE.FNS_FORMAT_STRING),
});
await waitForBatchedUpdatesWithAct();

// When the Subscription page card section renders
renderCardSection();
await waitForBatchedUpdatesWithAct();

// Then the existing pre-trial banner is unchanged, because the expiring soon check does not apply
expect(screen.getByText('PreTrialBillingBanner')).toBeOnTheScreen();
expect(screen.queryByText('SubscriptionExpiringSoonBanner')).not.toBeOnTheScreen();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm isn't it kind of weird to show a free trial banner if the user is already on a subscription?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@amyevans You are right. I was trying to quickly understand how pre-trial works, but I couldn't find when it should be displayed. The subscription page isn't available until the user creates a workspace, and when a user creates a workspace, the initial state is a free trial. This looks like old code or something controlled by backend NVP values.

It was added here: #43670

Since this has been the current behavior on main for a long time, I decided to keep it as is and not widen the PR scope.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@MelvinBot Can you find the actual test steps (not fake Onyx data) to display this banner? Also, is it intentional on the current main for the pre-trial banner to be displayed when the user is already on a subscription?

@MelvinBot

Copy link
Copy Markdown
Contributor Author

1. Real test steps: You can't fully reach this state without faking the end date. The app never sends or sets startDate or endDate. The backend sets endDate about 12 months after you switch to Annual. So the banner only appears about 11 months later, unless an internal engineer moves endDate on the backend. You can do everything else for real on web:

  1. As the owner of a paid workspace, upgrade it to Control. Collect on 2025 pricing doesn't offer Annual.
  2. Go to Account > Subscription > Edit subscription and choose Annual.
  3. Turn off Auto-renew. You may see a survey first.
  4. Fake only the date: Onyx.merge('nvp_private_subscription', {endDate: '<about 3 weeks from today>'}). Run it again after the Subscription page loads.

Caution: once you're on Annual, you may not be able to switch back to Pay-per-use.

2. Pre-trial banner for a subscriber: It's not intentional. It's an unhandled edge case. shouldShowPreTrialBillingBanner returns true whenever the trial dates are missing. It never checks the subscription. That was meant for a new workspace owner whose trial hadn't started yet. None of the PRs that built it mention paid subscribers. On the same file, canCancelSubscription treats missing trial dates as a paying subscriber. So an annual owner with no trial dates sees both Start a free trial and Cancel subscription.

This mostly affects older annual owners who never got trial dates. I couldn't confirm how many exist. A new owner gets trial dates as soon as the server finishes creating the workspace. That weakens my earlier reason for not adding a firstDayFreeTrial && guard. Let me know if you want it added to this PR.

Supporting details
  • Changing the subscription type sends only type. Turning off auto-renew sends only autoRenew. No client code writes the dates (Subscription.ts:85-190).
  • The app estimates a new term's end date as the first of the month, 12 months out (utils.ts:51).
  • The help site says raising the subscription size starts a new 12-month term. No user action brings the end date closer.
  • The pre-trial logic came from [Free trial] Implement and show Pre-Trial banner in the App during Pre-Trial. Its test steps cover only a new account that just created a workspace.

view run

@ahmedGaber93

Copy link
Copy Markdown
Contributor

Pre-trial banner for a subscriber: It's not intentional. It's an unhandled edge case. shouldShowPreTrialBillingBanner returns true whenever the trial dates are missing.

@MelvinBot Isn't this the correct behavior? If the trial dates are missing, shouldn't we consider the user to be in pre-trial? What's wrong with that?

@MelvinBot

Copy link
Copy Markdown
Contributor Author

For a new workspace owner, yes. Missing trial dates means pre-trial, and this PR doesn't change that.

The problem is that missing trial dates can also mean something else: an owner who is already paying and never had a trial. For example, an owner on an Annual subscription is paying by definition. Showing Start a free trial to them is wrong. shouldShowPreTrialBillingBanner can't tell these two cases apart because it never looks at the subscription.

The same file already handles this case differently. canCancelSubscription counts an owner as pre-trial only when firstDayFreeTrial exists. It treats an Annual owner with no trial dates as a paying subscriber and shows Cancel subscription. So for the same data, one helper says "hasn't started a trial" and the other says "paying." That mismatch is the edge case I meant.

In practice, this only matters if Annual owners without trial dates exist. I couldn't confirm that they do. If you're fine treating them as out of scope, no change is needed. This PR's reorder already covers the expiring-soon case.


view run

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@amyevans I think the best choice now is to remove that test case and leave shouldShowPreTrialBillingBanner as is, since this the current behaviour on production and it’s out of scope.

Please let me know if you’d like to handle it here.

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot remove this test case #102307 (comment) and leave shouldShowPreTrialBillingBanner as is.

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team as a code owner September 28, 2026 03:48
@melvin-bot
melvin-bot Bot requested a review from amyevans September 28, 2026 03:48
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done in e7aca72. I removed the still shows the pre-trial banner when the subscription is not about to expire test case from SubscriptionCardSectionBannerTest.tsx.

This PR doesn't change shouldShowPreTrialBillingBanner, so it stays as it is on main. The remaining expiring-soon test passes, and lint is clean.


view run

@melvin-bot

melvin-bot Bot commented Sep 28, 2026

Copy link
Copy Markdown

@amyevans 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 September 28, 2026 03:48
@codecov

codecov Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/libs/SubscriptionUtils.ts 88.12% <100.00%> (+0.10%) ⬆️
src/pages/settings/Subscription/FreeTrial.tsx 82.35% <100.00%> (ø)
...c/pages/settings/useInitialSettingsPageMenuData.ts 71.96% <100.00%> (ø)
.../settings/Subscription/CardSection/CardSection.tsx 71.91% <66.66%> (+71.91%) ⬆️
... and 42 files with indirect coverage changes

@trjExpensify

Copy link
Copy Markdown
Contributor

Yeah, this whole "pre-trial" thing I don't think is a case anymore is it? I believe a few years ago we didn't start the trial until the first action was taken, but we stopped doing that or something.

@garrettmknight
garrettmknight requested review from trjExpensify and removed request for garrettmknight September 28, 2026 13:08
@garrettmknight

Copy link
Copy Markdown
Contributor

@trjExpensify assigning you since you've been involved in the issue.

@amyevans

Copy link
Copy Markdown
Contributor

Yeah, this whole "pre-trial" thing I don't think is a case anymore is it? I believe a few years ago we didn't start the trial until the first action was taken, but we stopped doing that or something.

Yeah I am not entirely sure of the history, but I dug into it a bit with AI and we could not find any accounts that would fall into this in practice.

@amyevans I think the best choice now is to remove that test case and leave shouldShowPreTrialBillingBanner as is, since this the current behaviour on production and it’s out of scope.

Please let me know if you’d like to handle it here.

Agreed with your assessment, thanks for removing it!

@trjExpensify trjExpensify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cool, works for me too. 👍

@amyevans
amyevans merged commit 50457ee into main Sep 28, 2026
35 of 36 checks passed
@amyevans
amyevans deleted the claude-subscriptionExpiringSoonBannerPrecedence branch September 28, 2026 17:08
@github-actions

Copy link
Copy Markdown
Contributor

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

@OSBotify

Copy link
Copy Markdown
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
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/amyevans in version: 9.4.95-0 🚀

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

@MelvinBot

Copy link
Copy Markdown
Contributor Author

No help site changes are required for this PR, so I didn't create a docs PR.

Why

view run

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/Julesssss in version: 9.4.95-4 🚀

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

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

Labels

Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants