Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Checkout: Add "Is this purchase for business" checkbox for certain US states #88737

Open
wants to merge 15 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import CheckoutNextSteps from './checkout-next-steps';
import { CheckoutSidebarPlanUpsell } from './checkout-sidebar-plan-upsell';
import { EmptyCart, shouldShowEmptyCartPage } from './empty-cart';
import { GoogleDomainsCopy } from './google-transfers-copy';
import { IsForBusinessCheckbox } from './is-for-business-checkbox';
import JetpackAkismetCheckoutSidebarPlanUpsell from './jetpack-akismet-checkout-sidebar-plan-upsell';
import BeforeSubmitCheckoutHeader from './payment-method-step';
import SecondaryCartPromotions from './secondary-cart-promotions';
Expand Down Expand Up @@ -936,6 +937,7 @@ function CheckoutTermsAndCheckboxes( {
return (
<CheckoutTermsAndCheckboxesWrapper>
<BeforeSubmitCheckoutHeader />
<IsForBusinessCheckbox />
{ hasMarketplaceProduct && (
<AcceptTermsOfServiceCheckbox
isAccepted={ is3PDAccountConsentAccepted }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { FormStatus, useFormStatus } from '@automattic/composite-checkout';
import { useShoppingCart, convertTaxLocationToLocationUpdate } from '@automattic/shopping-cart';
import { CheckboxControl } from '@wordpress/components';
import { useTranslate } from 'i18n-calypso';
import useCartKey from '../../use-cart-key';

export function IsForBusinessCheckbox() {
const translate = useTranslate();
const { formStatus } = useFormStatus();

const cartKey = useCartKey();
const { responseCart, updateLocation, isLoading, isPendingUpdate } = useShoppingCart( cartKey );

const isUnitedStateWithBusinessOption = ( () => {
if ( responseCart.tax.location.country_code !== 'US' ) {
return false;
}
const zipCode = parseInt( responseCart.tax.location.postal_code ?? '0', 10 );
if ( zipCode >= 43000 && zipCode <= 45999 ) {
// Ohio; OH
return true;
}
if ( ( zipCode >= 6000 && zipCode <= 6389 ) || ( zipCode >= 6391 && zipCode <= 6999 ) ) {
// Connecticut; CT
return true;
}
return false;
} )();

const isChecked = responseCart.tax.location.is_for_business ?? false;
const isDisabled = formStatus !== FormStatus.READY || isLoading || isPendingUpdate;

if ( ! isUnitedStateWithBusinessOption ) {
return null;
}

return (
<CheckboxControl
id="checkout-is-business-checkbox"
label={ translate( 'Is this purchase for business?', { textOnly: true } ) }
checked={ isChecked }
disabled={ isDisabled }
onChange={ ( newValue ) => {
if ( isDisabled ) {
return;
}
updateLocation( {
...convertTaxLocationToLocationUpdate( responseCart.tax.location ),
isForBusiness: newValue,
} );
} }
/>
);
}
13 changes: 11 additions & 2 deletions client/my-sites/checkout/src/lib/translate-cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,16 @@ export function createTransactionEndpointCartFromResponseCart( {
function createTransactionEndpointTaxFromResponseCartTax(
tax: ResponseCartTaxData
): RequestCartTaxData {
const { country_code, postal_code, subdivision_code, vat_id, organization, address, city } =
tax.location;
const {
country_code,
postal_code,
subdivision_code,
vat_id,
organization,
address,
city,
is_for_business,
} = tax.location;
return {
location: {
country_code,
Expand All @@ -106,6 +114,7 @@ function createTransactionEndpointTaxFromResponseCartTax(
organization,
address,
city,
is_for_business,
},
};
}
Expand Down
6 changes: 5 additions & 1 deletion packages/shopping-cart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Regardless, it's a good idea to always check `responseCart.messages.errors` and
- `removeProductFromCart: ( uuidToRemove: string ) => Promise<ResponseCart>`. A function that requests removing a product from the cart.
- `applyCoupon: ( couponId: string ) => Promise<ResponseCart>`. A function that requests applying a coupon to the cart (only one coupon can be applied at a time).
- `removeCoupon: () => Promise<ResponseCart>`. A function that requests removing a coupon to the cart.
- `updateLocation: ( location: CartLocation ) => Promise<ResponseCart>`. A function that can be used to change the tax location of the cart.
- `updateLocation: ( location: LocationUpdate ) => Promise<ResponseCart>`. A function that can be used to change the tax location of the cart. Note that this completely replaces the current location. The [convertTaxLocationToLocationUpdate](#convertTaxLocationToLocationUpdate) function can be used to convert the current `responseCart.tax.location` value into the properties required by this function if you need to only update one value.
- `replaceProductInCart: ( uuidToReplace: string, productPropertiesToChange: Partial< RequestCartProduct > ) => Promise<ResponseCart>`. A function that can replace one product in the cart with another, retaining the same UUID; useful for changing product variants.
- `replaceProductsInCart: ( products: RequestCartProduct[] ) => Promise<ResponseCart>`. A function that replaces all the products in the cart with a new set of products. Can also be used to clear the cart.
- `reloadFromServer: () => Promise<ResponseCart>`. A function to throw away the current cart cache and fetch it fresh from the shopping cart API.
Expand Down Expand Up @@ -102,3 +102,7 @@ A `ShoppingCartManager` has the following properties:
- `actions: ShoppingCartManagerActions`. An object whose properties are the various actions that can be taken on the cart. They are the same as the actions returned by [useShoppingCart](#useShoppingCart).
- `subscribe: ( callback: () => void ) => () => void`. A function to subscribe to updates to a `ShoppingCartManager` for a given cart key. The `callback` will be called any time the `ShoppingCartManager` changes for that key. The return value of the function is an unsubscribe function.
- `fetchInitialCart: () => Promise<ResponseCart>`. A function that should be called after the cart manager is created in order to perform the initial fetch. If another action is called first, this will be called automatically before that action is dispatched.

## convertTaxLocationToLocationUpdate

Converts the `responseCart.tax.location` data in a `ResponseCart` to the data required by the `updateLocation()` cart action.
64 changes: 51 additions & 13 deletions packages/shopping-cart/src/cart-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import debugFactory from 'debug';
import { getEmptyResponseCart } from './empty-carts';
import type {
TempResponseCart,
CartLocation,
TaxLocationUpdate,
RequestCart,
RequestCartProduct,
ResponseCart,
ResponseCartProduct,
GetCart,
CartKey,
ResponseCartTaxLocation,
} from './types';

const debug = debugFactory( 'shopping-cart:cart-functions' );
Expand Down Expand Up @@ -43,7 +44,8 @@ export function convertResponseCartToRequestCart( {
tax.location.vat_id ||
tax.location.organization ||
tax.location.address ||
tax.location.city
tax.location.city ||
tax.location.is_for_business
) {
requestCartTax = {
location: {
Expand All @@ -54,6 +56,7 @@ export function convertResponseCartToRequestCart( {
organization: tax.location.organization,
address: tax.location.address,
city: tax.location.city,
is_for_business: tax.location.is_for_business,
},
};
}
Expand Down Expand Up @@ -110,30 +113,60 @@ export function removeCouponFromResponseCart( cart: TempResponseCart ): TempResp
};
}

/**
* Convert the `tax.location` data in a response cart to the data required by
* the `updateLocation()` cart action.
*/
export function convertTaxLocationToLocationUpdate(
location: ResponseCartTaxLocation
): TaxLocationUpdate {
return {
countryCode: location.country_code || undefined,
postalCode: location.postal_code || undefined,
subdivisionCode: location.subdivision_code || undefined,
vatId: location.vat_id || undefined,
organization: location.organization || undefined,
address: location.address || undefined,
city: location.city || undefined,
isForBusiness: location.is_for_business || undefined,
};
}

/**
* Convert the tax location data used by the `updateLocation()` cart action to
* the `tax.location` data in the response cart.
*/
export function convertLocationUpdateToTaxLocation(
location: TaxLocationUpdate
): ResponseCartTaxLocation {
return {
country_code: location.countryCode || undefined,
postal_code: location.postalCode || undefined,
subdivision_code: location.subdivisionCode || undefined,
vat_id: location.vatId || undefined,
organization: location.organization || undefined,
address: location.address || undefined,
city: location.city || undefined,
is_for_business: location.isForBusiness || undefined,
};
}

export function addLocationToResponseCart(
cart: TempResponseCart,
location: CartLocation
location: TaxLocationUpdate
): TempResponseCart {
return {
...cart,
tax: {
...cart.tax,
location: {
country_code: location.countryCode || undefined,
postal_code: location.postalCode || undefined,
subdivision_code: location.subdivisionCode || undefined,
vat_id: location.vatId || undefined,
organization: location.organization || undefined,
address: location.address || undefined,
city: location.city || undefined,
},
location: convertLocationUpdateToTaxLocation( location ),
},
};
}

export function doesCartLocationDifferFromResponseCartLocation(
cart: TempResponseCart,
location: CartLocation
location: TaxLocationUpdate
): boolean {
const {
countryCode: newCountryCode = '',
Expand All @@ -143,6 +176,7 @@ export function doesCartLocationDifferFromResponseCartLocation(
organization: newOrganization = '',
address: newAddress = '',
city: newCity = '',
isForBusiness: newIsForBusiness = false,
} = location;
const {
country_code: oldCountryCode = '',
Expand All @@ -152,6 +186,7 @@ export function doesCartLocationDifferFromResponseCartLocation(
organization: oldOrganization = '',
address: oldAddress = '',
city: oldCity = '',
is_for_business: oldIsForBusiness = false,
} = cart.tax?.location ?? {};

if ( location.countryCode !== undefined && newCountryCode !== oldCountryCode ) {
Expand All @@ -175,6 +210,9 @@ export function doesCartLocationDifferFromResponseCartLocation(
if ( location.city !== undefined && newCity !== oldCity ) {
return true;
}
if ( location.isForBusiness !== undefined && newIsForBusiness !== oldIsForBusiness ) {
return true;
}
return false;
}

Expand Down
5 changes: 4 additions & 1 deletion packages/shopping-cart/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ export * from './shopping-cart-manager';
export * from './empty-carts';
export * from './types';
export * from './errors';
export { convertResponseCartToRequestCart } from './cart-functions';
export {
convertResponseCartToRequestCart,
convertTaxLocationToLocationUpdate,
} from './cart-functions';
41 changes: 29 additions & 12 deletions packages/shopping-cart/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export type ApplyCouponToCart = ( couponId: string ) => Promise< ResponseCart >;

export type RemoveProductFromCart = ( uuidToRemove: string ) => Promise< ResponseCart >;

export type UpdateTaxLocationInCart = ( location: CartLocation ) => Promise< ResponseCart >;
export type UpdateTaxLocationInCart = ( location: TaxLocationUpdate ) => Promise< ResponseCart >;

export type SetCouponFieldVisible = ( couponFieldVisible: boolean ) => void;

Expand Down Expand Up @@ -118,7 +118,7 @@ export type ShoppingCartAction =
| { type: 'REMOVE_CART_ITEM'; uuidToRemove: string }
| { type: 'CART_PRODUCTS_ADD'; products: RequestCartProduct[] }
| { type: 'CART_PRODUCTS_REPLACE_ALL'; products: RequestCartProduct[] }
| { type: 'SET_LOCATION'; location: CartLocation }
| { type: 'SET_LOCATION'; location: TaxLocationUpdate }
| {
type: 'CART_PRODUCT_REPLACE';
uuidToReplace: string;
Expand Down Expand Up @@ -211,6 +211,7 @@ export type RequestCartTaxData = null | {
organization?: string;
address?: string;
city?: string;
is_for_business?: boolean;
};
};

Expand Down Expand Up @@ -315,16 +316,19 @@ export interface ResponseCart< P = ResponseCartProduct > {
has_pending_payment?: boolean;
}

export interface ResponseCartTaxLocation {
country_code?: string;
postal_code?: string;
subdivision_code?: string;
vat_id?: string;
organization?: string;
address?: string;
city?: string;
is_for_business?: boolean;
}

export interface ResponseCartTaxData {
location: {
country_code?: string;
postal_code?: string;
subdivision_code?: string;
vat_id?: string;
organization?: string;
address?: string;
city?: string;
};
location: ResponseCartTaxLocation;
display_taxes: boolean;
}

Expand Down Expand Up @@ -584,16 +588,29 @@ export interface IntroductoryOfferTerms {
should_prorate_when_offer_ends: boolean;
}

export interface CartLocation {
/**
* The data passed to the `updateLocation()` cart manager action.
*
* To convert this to the data used by the cart's `tax.location` property, use
* `convertLocationUpdateToTaxLocation()`.
*/
export interface TaxLocationUpdate {
countryCode?: string;
postalCode?: string;
subdivisionCode?: string;
vatId?: string;
organization?: string;
address?: string;
city?: string;
isForBusiness?: boolean;
}

/**
* Legacy alias for `TaxLocationUpdate`.
* @deprecated Use `TaxLocationUpdate`
*/
export type CartLocation = TaxLocationUpdate;

export type DomainLegalAgreementUrl = string;
export type DomainLegalAgreementTitle = string;
export type DomainLegalAgreements = Record< DomainLegalAgreementUrl, DomainLegalAgreementTitle >;
Expand Down
3 changes: 3 additions & 0 deletions packages/wpcom-checkout/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export type WPCOMTransactionEndpointPaymentDetails = {
streetNumber?: string;
phoneNumber?: string;
document?: string;
isForBusiness?: boolean;
deviceId?: string;
successUrl?: string;
cancelUrl?: string;
Expand Down Expand Up @@ -562,6 +563,7 @@ export type DomainContactValidationRequestExtraFields = {
trademark_number?: string;
siren_siret?: string;
};
is_for_business?: boolean;
};

export type ContactValidationResponseMessagesExtra = {
Expand All @@ -580,6 +582,7 @@ export type ContactValidationResponseMessagesExtra = {
trademark_number?: string[];
siren_siret?: string[];
};
is_for_business?: boolean;
};

/**
Expand Down
Loading