Skip to content

Conversation

@yashwin
Copy link
Contributor

@yashwin yashwin commented Dec 24, 2025

Part of https://linear.app/a8c/issue/A4A-1926/marketplace-show-monthlyearly-pricing-for-hosting-and-products

Resolves https://linear.app/a8c/issue/A4A-1929/products-display-monthlyyearly-pricing-on-cards
Resolves https://linear.app/a8c/issue/A4A-1930/hosting-display-monthlyyearly-pricing-on-cards
Resolves https://linear.app/a8c/issue/A4A-1961/cart-display-monthlyyearly-pricing
Resolves https://linear.app/a8c/issue/A4A-1962/display-term-based-pricing-on-product-details-modal-lightbox

Proposed Changes

This PR implements term pricing for Hosting, Products & Cart.

Why are these changes being made?

  • To support term pricing on A4A Marketplace

Testing Instructions

Note: There might be some pricing difference when the feature flag is enabled. Please ignore it now, as we will fix it later.

  • Open the A4A live link
  • Use a non-BD agency > Test all the scenarios mentioned below > Verify the prices match production, and all the flows work as expected.
  • Use a BD agency > Append the URL with ?flags=-a4a-bd-term-pricing
  • Test all the scenarios mentioned below > Verify the prices match production, and all the flows work as expected.
  1. Marketplace > Hosting > Standard Agency Hosting > Refer mode on/off
  2. Marketplace > Hosting > Premier Agency Hosting > Refer mode on/off
  3. Marketplace > Products > Refer mode on/off
  4. Marketplace > Products > View details for a product
  5. Marketplace > Checkout
  • Verify that all the filters work as expected on the Products page

  • Patch 199438-ghe-Automattic/wpcom/

  • Append the URL with ?flags=a4a-bd-term-pricing > Test all the scenarios mentioned below > Verify the UI looks as displayed below for yearly and monthly billing.

Billing term: Monthly

  1. Marketplace > Hosting > Standard Agency Hosting > Refer mode off
Screenshot 2026-01-14 at 2 48 42 PM
  1. Marketplace > Hosting > Standard Agency Hosting > Refer mode on
Screenshot 2026-01-14 at 2 53 55 PM
  1. Marketplace > Hosting > Premier Agency Hosting > Refer mode on
Screenshot 2026-01-14 at 2 54 31 PM
  1. Marketplace > Hosting > Premier Agency Hosting > Refer mode off
Screenshot 2026-01-14 at 2 55 27 PM
  1. Marketplace > Products > Refer mode off
Screenshot 2026-01-14 at 2 57 19 PM
  1. Marketplace > Products > Refer mode on
Screenshot 2026-01-14 at 2 57 54 PM
  1. Cart > Refer mode off
Screenshot 2026-01-14 at 2 58 54 PM
  1. Cart > Refer mode on
Screenshot 2026-01-14 at 2 59 06 PM
  1. View details
Screenshot 2026-01-14 at 3 04 54 PM

Billing term: Yearly

  1. Marketplace > Hosting > Standard Agency Hosting > Refer mode off
Screenshot 2026-01-14 at 3 00 53 PM
  1. Marketplace > Hosting > Standard Agency Hosting > Refer mode on
Screenshot 2026-01-14 at 3 01 04 PM
  1. Marketplace > Hosting > Premier Agency Hosting > Refer mode off
Screenshot 2026-01-14 at 3 01 27 PM
  1. Marketplace > Hosting > Premier Agency Hosting > Refer mode on
Screenshot 2026-01-14 at 3 02 00 PM
  1. Marketplace > Products > Refer mode off
Screenshot 2026-01-14 at 3 03 04 PM
  1. Marketplace > Products > Refer mode on
Screenshot 2026-01-14 at 3 03 16 PM
  1. Cart > Refer mode off
Screenshot 2026-01-14 at 3 03 30 PM
  1. Cart > Refer mode on
Screenshot 2026-01-14 at 3 03 41 PM
  1. View details
Screenshot 2026-01-14 at 3 04 43 PM

Pre-merge Checklist

  • Has the general commit checklist been followed? (PCYsg-hS-p2)
  • Have you written new tests for your changes?
  • Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
  • Have you checked for TypeScript, React or other console errors?
  • Have you tested accessibility for your changes? Ensure the feature remains usable with various user agents (e.g., browsers), interfaces (e.g., keyboard navigation), and assistive technologies (e.g., screen readers) (PCYsg-S3g-p2).
  • Have you used memoizing on expensive computations? More info in Memoizing with create-selector and Using memoizing selectors and Our Approach to Data
  • Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    • For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages.
  • For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?

@yashwin yashwin requested a review from a team December 24, 2025 07:09
@yashwin yashwin self-assigned this Dec 24, 2025
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Dec 24, 2025
@matticbot
Copy link
Contributor

matticbot commented Dec 24, 2025

Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

App Entrypoints (~108 bytes added 📈 [gzipped])

Details
name                    parsed_size           gzip_size
entry-main                  +1206 B  (+0.0%)     +108 B  (+0.0%)
entry-subscriptions          +126 B  (+0.0%)      +43 B  (+0.0%)
entry-stepper                +126 B  (+0.0%)      +43 B  (+0.0%)
entry-reauth-required        +126 B  (+0.0%)      +43 B  (+0.0%)
entry-login                  +126 B  (+0.0%)      +43 B  (+0.0%)
entry-domains-landing        +126 B  (+0.0%)      +43 B  (+0.0%)
entry-dashboard-dotcom       +126 B  (+0.0%)      +43 B  (+0.0%)
entry-dashboard-ciab         +126 B  (+0.0%)      +43 B  (+0.0%)
entry-browsehappy            +126 B  (+0.1%)      +43 B  (+0.1%)

Common code that is always downloaded and parsed every time the app is loaded, no matter which route is used.

Sections (~48 bytes added 📈 [gzipped])

Details
name                               parsed_size           gzip_size
scan                                  +24568 B  (+2.6%)    +7094 B  (+2.5%)
jetpack-social                        +24568 B  (+3.4%)    +7094 B  (+3.3%)
jetpack-search                        +24568 B  (+3.7%)    +7094 B  (+3.7%)
backup                                +24568 B  (+1.9%)    +7094 B  (+1.9%)
jetpack-cloud-manage-pricing          +24435 B  (+5.8%)    +7100 B  (+5.8%)
jetpack-cloud-partner-portal          +23933 B  (+2.2%)    +7106 B  (+2.2%)
jetpack-cloud-agency-dashboard        +22627 B  (+2.0%)    +6655 B  (+2.0%)
a8c-for-agencies-sites                +21771 B  (+0.6%)    +6349 B  (+0.7%)
a8c-for-agencies-marketplace           +3982 B  (+0.2%)    +1775 B  (+0.3%)
a8c-for-agencies-client                +2050 B  (+0.1%)    +1290 B  (+0.2%)
a8c-for-agencies-purchases              +416 B  (+0.0%)     +286 B  (+0.1%)
a8c-for-agencies-referrals              +334 B  (+0.0%)      +56 B  (+0.0%)
a8c-for-agencies-overview               +221 B  (+0.0%)     +234 B  (+0.0%)
checkout                                +115 B  (+0.0%)      +29 B  (+0.0%)
a8c-for-agencies-express-checkout       +115 B  (+0.0%)      +37 B  (+0.0%)

Sections contain code specific for a given set of routes. Is downloaded and parsed only when a particular route is navigated to.

Legend

What is parsed and gzip size?

Parsed Size: Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory.
Gzip Size: Compressed size of the JS and CSS files. This much data needs to be downloaded over network.

Generated by performance advisor bot at iscalypsofastyet.com.

@jkguidaven
Copy link
Contributor

@yashwin, I tested it, and it works just fine.

My main concern is that we are doing a lot of conditional rendering. Do you think we can just calculate the value (discount and actual cost) within getProductPricingInfo so we have fewer changes on the rendering logic?

const isFree = actualCost === 0;

const renderPrice = () => {
if ( isTermPricingEnabled ) {
Copy link
Contributor

@jkguidaven jkguidaven Jan 7, 2026

Choose a reason for hiding this comment

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

I can see that we are doing conditional rendering with the prices based on the term selected. I wonder if we could just handle the calculation from getProductPricingInfo so we don't have to do conditional rendering here and other places. wdyt?

@yashwin yashwin force-pushed the update/a4a/marketplace/implement-term-pricing-for-products-and-hosting branch from 288500f to 21a9dbb Compare January 8, 2026 08:16
@yashwin
Copy link
Contributor Author

yashwin commented Jan 8, 2026

Thanks for the review, @jkguidaven!

I have made the changes as per your suggestions. Could you please take another look?

@yashwin yashwin force-pushed the update/a4a/marketplace/implement-term-pricing-for-products-and-hosting branch from 21a9dbb to 1b74cb2 Compare January 12, 2026 03:50
@cleacos
Copy link
Contributor

cleacos commented Jan 13, 2026

this PR needs a rebase

@yashwin yashwin force-pushed the update/a4a/marketplace/implement-term-pricing-for-products-and-hosting branch from 1b74cb2 to 8142181 Compare January 14, 2026 05:53
@yashwin yashwin changed the title A4A > Marketplace: Implement placeholder for term pricing for Hosting, Products & Cart A4A > Marketplace: Implement term pricing for Hosting, Products & Cart Jan 14, 2026
@yashwin yashwin force-pushed the update/a4a/marketplace/implement-term-pricing-for-products-and-hosting branch from 8142181 to c1f39f6 Compare January 14, 2026 09:16
import { getActiveAgency } from 'calypso/state/a8c-for-agencies/agency/selectors';
import { TitanOrder } from 'calypso/state/a8c-for-agencies/types';
import { APIProductFamilyProduct } from 'calypso/state/partner-portal/types';
import type { APIProductFamilyProduct } from 'calypso/a8c-for-agencies/types/products';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We created a new types file on A4A as we are introducing new fields.

} );
}

export function usePublicProductsQuery(): UseQueryResult< APIProductFamilyProduct[], unknown > {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are not using this anywhere.

const getProductsQueryKey = ( agencyId?: number ) => [ 'a4a', 'marketplace', 'products', agencyId ];

export default function useProductsQuery(
isPublicFacing = false,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed support for the public-facing API as we no longer need it.

productSearchQuery,
usePublicQuery = false,
}: Props ) {
const { data, isLoading: isLoadingProducts } = useProductsQuery( usePublicQuery );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

usePublicQuery is not used anywhere.

export function useProductCategories( product: APIProductFamilyProduct ): string[] {
const translate = useTranslate();
const { family_slug } = product;
const { family_slug, slug } = product;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are going to change how we group families. You can see the response here: 199438-ghe-Automattic/wpcom

To support that, we are going to use the slug also so that the current implementation doesn't break.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's ok to repeat the same return here as we will get rid of the logic completely when we enable BD for all the users.

</>
);
};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixing some TS warnings

return 0;
};

export const calculateCommissions = ( referral: Referral, products: APIProductFamilyProduct[] ) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not being used anymore


export const getProductCommissionPercentage = ( slug?: string ) => {
if ( ! slug || ! isProductEligibleForCommission( slug ) ) {
export const getProductCommissionPercentage = ( slug?: string, familySlug?: string ) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As mentioned earlier, we will change the family structure. This is to correctly calculate the commissions.

Copy link
Contributor

Choose a reason for hiding this comment

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

since familySlug was added do we plan to get rid of slug or something? Or are they both needed?

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, both are needed

@yashwin yashwin force-pushed the update/a4a/marketplace/implement-term-pricing-for-products-and-hosting branch 2 times, most recently from 0208ea9 to 029f8f3 Compare January 14, 2026 10:32
@cleacos cleacos self-requested a review January 14, 2026 10:39
@cleacos
Copy link
Contributor

cleacos commented Jan 14, 2026

I tested it, using the new endpoint, it seems that it works well, but my checkout is configured to use EUR, so in the marketplace the prices are in USD, and in the checkout it is EUR.


const isTermPricingEnabled = isEnabled( 'a4a-bd-term-pricing' ) && isEnabled( 'a4a-bd-checkout' );

const priceInterval = () => {
Copy link
Contributor

@jkguidaven jkguidaven Jan 14, 2026

Choose a reason for hiding this comment

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

Is there a way we could make this reusable? This seems to be repeated 3 times in different places. could be part of getProductPricingInfo?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd tried doing it. The thing is, this will be removed soon, so we will only have term pricing going forward. So, I would leave this as-is for now. Also, it is repeated in 2 places only, the text is different in another place.

@yashwin yashwin force-pushed the update/a4a/marketplace/implement-term-pricing-for-products-and-hosting branch from da7bb97 to 31110d8 Compare January 15, 2026 06:52
@yashwin
Copy link
Contributor Author

yashwin commented Jan 15, 2026

Thanks for the review, @cleacos & @jkguidaven!

I found the filters weren't working properly. I have fixed it now: 31110d8

I tested it, using the new endpoint, it seems that it works well, but my checkout is configured to use EUR, so in the marketplace the prices are in USD, and in the checkout it is EUR.

@cleacos This is based on the endpoint, not the UI. UI is configured to work with the product's currency. I have now updated the endpoint to take the current based on the agency. So, this should work fine now.

@yashwin yashwin force-pushed the update/a4a/marketplace/implement-term-pricing-for-products-and-hosting branch from dc80259 to e6d9019 Compare January 16, 2026 04:25
@cleacos cleacos requested a review from travisw January 16, 2026 12:29
@cleacos
Copy link
Contributor

cleacos commented Jan 16, 2026

@yashwin I'm still facing this issue for WPCOM plans:

image

I added the WPCOM hosting plan to the cart, then I went to the checkout page, and it failed.

@cleacos
Copy link
Contributor

cleacos commented Jan 16, 2026

Now this:

image

@yashwin
Copy link
Contributor Author

yashwin commented Jan 19, 2026

@cleacos: Could you please try again?

I tested it locally for both monthly and yearly products and it works fine. Please ensure to get the latest trunk and restart the server when you switch to this branch.

Screenshot 2026-01-19 at 11 56 45 AM

@yashwin yashwin force-pushed the update/a4a/marketplace/implement-term-pricing-for-products-and-hosting branch from 57d4684 to 7b72516 Compare January 19, 2026 06:36
product_id: number;
monthly_product_id?: number;
yearly_product_id?: number;
alternative_product_id?: number;
Copy link
Contributor

@cleacos cleacos Jan 19, 2026

Choose a reason for hiding this comment

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

Missed fields?

  • monthly_alternative_product_id
  • yearly_alternative_product_id

and remove alternative_product_id?

I had this comment from the previous version

@@ -1,9 +1,8 @@
export const SECURITY_PRODUCT_SLUGS = [
'jetpack-backup',
'jetpack-backup-t1',
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 that Jetpack Backup products could be T1 or T2.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, we only show T1, right?

@yashwin yashwin force-pushed the update/a4a/marketplace/implement-term-pricing-for-products-and-hosting branch 2 times, most recently from a8439bf to 5711f86 Compare January 19, 2026 08:57
@matticbot
Copy link
Contributor

matticbot commented Jan 19, 2026

This PR modifies the release build for the following Calypso Apps:

For info about this notification, see here: PCYsg-OT6-p2

  • agents-manager
  • help-center
  • notifications
  • wpcom-block-editor

To test WordPress.com changes, run install-plugin.sh $pluginSlug update/a4a/marketplace/implement-term-pricing-for-products-and-hosting on your sandbox.

@cleacos
Copy link
Contributor

cleacos commented Jan 19, 2026

There is a mismatch between the WPCOM Hosting prices. For example, the base monthly price in EUR should be 22 EUR and the endpoint returns 40

@yashwin yashwin force-pushed the update/a4a/marketplace/implement-term-pricing-for-products-and-hosting branch from 5711f86 to f2b10d3 Compare January 19, 2026 15:55
Copy link
Contributor

@travisw travisw left a comment

Choose a reason for hiding this comment

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

I didn't inspect every line of code but in general the actual changes in functionality seem reasonable. And in testing it seemed ok.

I also left a couple comment on the back pr about incorrect prices and using different product ids in the endpoint (200442-ghe-Automattic/wpcom).


export const getProductCommissionPercentage = ( slug?: string ) => {
if ( ! slug || ! isProductEligibleForCommission( slug ) ) {
export const getProductCommissionPercentage = ( slug?: string, familySlug?: string ) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

since familySlug was added do we plan to get rid of slug or something? Or are they both needed?

@travisw
Copy link
Contributor

travisw commented Jan 21, 2026

Use a non-BD agency > Test all the scenarios mentioned below > Verify the prices match production, and all the flows work as expected.

To clarify, non-BD agencies should be unchanged, right? No term pricing, prices still using the old endpoint, ...

@yashwin
Copy link
Contributor Author

yashwin commented Jan 21, 2026

Thanks for the review, @travisw!

since familySlug was added do we plan to get rid of slug or something? Or are they both needed?

Yes, both are needed for the filters to work properly.

To clarify, non-BD agencies should be unchanged, right? No term pricing, prices still using the old endpoint

Yes, correct. No term pricing, no BD checkout.

I also left a couple comment on the back pr about incorrect prices and using different product ids in the endpoint (200442-ghe-Automattic/wpcom).

I'll address that separately.

@cleacos
Copy link
Contributor

cleacos commented Jan 21, 2026

Pretty close!

I found a rounding issue applying the WPCOM Hosting discount prices:

image

VS

image

@yashwin
Copy link
Contributor Author

yashwin commented Jan 21, 2026

Pretty close!

I found a rounding issue applying the WPCOM Hosting discount prices:

I'll fix this in another PR

Copy link
Contributor

@travisw travisw left a comment

Choose a reason for hiding this comment

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

Seems good to ship

@yashwin
Copy link
Contributor Author

yashwin commented Jan 23, 2026

Closing this in favour of #108280

@yashwin yashwin closed this Jan 23, 2026
@github-actions github-actions bot removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Jan 23, 2026
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