Skip to content

Commit

Permalink
Merge pull request #17334 from mozilla/FXA-7579
Browse files Browse the repository at this point in the history
fix(libs): Add Coupon component
  • Loading branch information
xlisachan authored Sep 11, 2024
2 parents a4e1781 + e66d0a6 commit c33d786
Show file tree
Hide file tree
Showing 21 changed files with 373 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { headers } from 'next/headers';

import { CouponForm } from '@fxa/payments/ui';
import {
getApp,
fetchCMSData,
getApp,
getCartAction,
PurchaseDetails,
SubscriptionTitle,
Expand Down Expand Up @@ -67,6 +68,12 @@ export default async function RootLayout({
cms.defaultPurchase.data.attributes.purchaseDetails.data.attributes
}
/>
<CouponForm
cartId={cart.id}
cartVersion={cart.version}
promoCode={cart.couponCode}
readOnly={false}
/>
</section>

<div className="page-body rounded-t-lg tablet:rounded-t-none">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { revalidatePath } from 'next/cache';
import { headers } from 'next/headers';
import { PaymentSection } from '@fxa/payments/ui';
import { BaseButton, ButtonVariant, PaymentSection } from '@fxa/payments/ui';
import {
getApp,
getCartOrRedirectAction,
Expand All @@ -16,7 +16,6 @@ import {
getCMSContent,
} from 'apps/payments/next/app/_lib/apiClient';
import { auth, signIn } from 'apps/payments/next/auth';
import { PrimaryButton } from 'libs/payments/ui/src/lib/client/components/PrimaryButton';
import { CheckoutParams } from '../layout';

export const dynamic = 'force-dynamic';
Expand Down Expand Up @@ -110,7 +109,14 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
name="email"
className="w-full border rounded-md border-black/30 p-3 placeholder:text-grey-500 placeholder:font-normal focus:border focus:!border-black/30 focus:!shadow-[0_0_0_3px_rgba(10,132,255,0.3)] focus-visible:outline-none data-[invalid=true]:border-alert-red data-[invalid=true]:text-alert-red data-[invalid=true]:shadow-inputError"
/>
<PrimaryButton type="submit"> Set email</PrimaryButton>
<BaseButton
className="mt-10 w-full"
type="submit"
variant={ButtonVariant.Primary}
>
{' '}
Set email
</BaseButton>
</form>
</div>

Expand Down
4 changes: 2 additions & 2 deletions apps/payments/next/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ export default function Index() {
<h2 className="text-xl mt-8">Without auth</h2>
<div className="flex gap-8">
<div className="flex flex-col gap-2 p-4 items-center">
<h2>VPN - Monthly</h2>
<h2>123Done Pro - Monthly</h2>
<Link
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
href="/en/vpn/checkout/monthly/new"
href="/en/123donepro/checkout/monthly/new"
>
Redirect
</Link>
Expand Down
2 changes: 1 addition & 1 deletion apps/payments/next/app/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ body {
}

.page-body {
@apply component-card border-t-0 mb-6 pt-4 px-4 pb-14 text-grey-600 desktop:px-12 desktop:pb-12;
@apply bg-white rounded-b-lg shadow-sm shadow-grey-300 border-t-0 mb-6 pt-4 px-4 pb-14 text-grey-600 desktop:px-12 desktop:pb-12;
}

.page-header {
Expand Down
6 changes: 5 additions & 1 deletion libs/payments/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@

// Use this file to export React client components (e.g. those with 'use client' directive) or other non-server utilities

export * from './lib/utils/helpers';
export * from './lib/client/components/BaseButton';
export * from './lib/client/components/CheckoutForm';
export * from './lib/client/components/CheckoutCheckbox';
export * from './lib/client/components/CouponForm';
export * from './lib/client/components/PaymentSection';
export * from './lib/client/components/SubmitButton';
export * from './lib/client/providers/Providers';
export * from './lib/utils/helpers';
export * from './lib/utils/types';
9 changes: 8 additions & 1 deletion libs/payments/ui/src/lib/actions/updateCart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

'use server';

import { plainToClass } from 'class-transformer';
import { revalidatePath } from 'next/cache';

import { UpdateCart } from '@fxa/payments/cart';
import { getApp } from '../nestapp/app';
import { UpdateCartActionArgs } from '../nestapp/validators/UpdateCartActionArgs';
import { plainToClass } from 'class-transformer';

export const updateCartAction = async (
cartId: string,
Expand All @@ -23,4 +25,9 @@ export const updateCartAction = async (
cartDetails,
})
);

revalidatePath(
'/[locale]/[offeringId]/checkout/[interval]/[cartId]/start',
'page'
);
};
34 changes: 34 additions & 0 deletions libs/payments/ui/src/lib/client/components/BaseButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

export enum ButtonVariant {
Primary,
Secondary,
}

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
variant?: ButtonVariant;
}

export function BaseButton({ children, variant, ...props }: ButtonProps) {
let variantStyles = '';
switch (variant) {
case ButtonVariant.Primary:
variantStyles = 'bg-blue-500 hover:bg-blue-700 text-white';
break;
case ButtonVariant.Secondary:
variantStyles = 'bg-grey-100 hover:bg-grey-200 text-black';
break;
}

return (
<button
{...props}
className={`flex items-center justify-center font-semibold h-12 rounded-md p-4 z-10 aria-disabled:relative aria-disabled:after:absolute aria-disabled:after:content-[''] aria-disabled:after:top-0 aria-disabled:after:left-0 aria-disabled:after:w-full aria-disabled:after:h-full aria-disabled:after:bg-white aria-disabled:after:opacity-50 aria-disabled:after:z-30 aria-disabled:border-none ${props.className} ${variantStyles}`}
>
{children}
</button>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
'use client';

import { Localized, useLocalization } from '@fluent/react';
import { PayPalButtons } from '@paypal/react-paypal-js';
import * as Form from '@radix-ui/react-form';
import {
PaymentElement,
Expand All @@ -14,12 +15,11 @@ import { StripePaymentElementChangeEvent } from '@stripe/stripe-js';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';

import { BaseButton, ButtonVariant, CheckoutCheckbox } from '@fxa/payments/ui';
import LockImage from '@fxa/shared/assets/images/lock.svg';
import { CheckoutCheckbox } from '../CheckoutCheckbox';
import { PrimaryButton } from '../PrimaryButton';
import { checkoutCartWithStripe } from '../../../actions/checkoutCartWithStripe';
import { handleStripeErrorAction } from '../../../actions/handleStripeError';
import { PayPalButtons } from '@paypal/react-paypal-js';

interface CheckoutFormProps {
cmsCommonContent: {
Expand Down Expand Up @@ -251,15 +251,17 @@ export function CheckoutForm({
className="mt-6"
/>
) : (
<PrimaryButton
<BaseButton
className="mt-10 w-full"
type="submit"
variant={ButtonVariant.Primary}
aria-disabled={
!stripeFieldsComplete || !nonStripeFieldsComplete || loading
}
>
<Image src={LockImage} className="h-4 w-4 mx-3" alt="" />
<Localized id="next-new-user-submit">Subscribe Now</Localized>
</PrimaryButton>
</BaseButton>
)}
</Form.Submit>
)}
Expand Down
13 changes: 13 additions & 0 deletions libs/payments/ui/src/lib/client/components/CouponForm/en.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Component - CouponForm

next-coupon-enter-code =
.placeholder = Enter Code
# Title of container where a user can input a coupon code to get a discount on a subscription.
next-coupon-promo-code = Promo Code
# Title of container showing discount coupon code applied to a subscription.
next-coupon-promo-code-applied = Promo Code Applied
next-coupon-remove = Remove
next-coupon-submit = Apply
155 changes: 155 additions & 0 deletions libs/payments/ui/src/lib/client/components/CouponForm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

'use client';

import { Localized } from '@fluent/react';
import { ButtonVariant } from '../BaseButton';
import { SubmitButton } from '../SubmitButton';
import { updateCartAction } from '../../../actions/updateCart';
import { getFallbackTextByFluentId } from '../../../utils/error-ftl-messages';

interface WithCouponProps {
cartId: string;
cartVersion: number;
couponCode: string;
readOnly: boolean;
}
const WithCoupon = ({
cartId,
cartVersion,
couponCode,
readOnly,
}: WithCouponProps) => {
async function removeCoupon() {
await updateCartAction(cartId, cartVersion, { couponCode: '' });
}

return (
<>
<h2 className="m-0 mb-4 font-semibold text-grey-600">
<Localized id="next-coupon-promo-code-applied">
Promo Code Applied
</Localized>
</h2>

<form
action={removeCoupon}
className="flex gap-4 justify-between items-center"
data-testid="coupon-hascoupon"
>
<span className="break-all">{couponCode}</span>
{readOnly ? null : (
<span>
<SubmitButton
className="w-24"
variant={ButtonVariant.Secondary}
data-testid="coupon-remove-button"
>
<Localized id="next-coupon-remove">Remove</Localized>
</SubmitButton>
</span>
)}
</form>
</>
);
};

interface WithoutCouponProps {
cartId: string;
cartVersion: number;
readOnly: boolean;
}

const WithoutCoupon = ({
cartId,
cartVersion,
readOnly,
}: WithoutCouponProps) => {
async function applyCoupon(formData: FormData) {
const promotionCode = formData.get('coupon') as string;

await updateCartAction(cartId, cartVersion, {
couponCode: promotionCode,
});
}

const error = null;

return (
<>
<h2 className="m-0 mb-4 font-semibold text-grey-600">
<Localized id="next-coupon-promo-code">Promo Code</Localized>
</h2>

<form action={applyCoupon} data-testid="coupon-form">
<div className="flex gap-4 justify-between items-center">
<Localized attrs={{ placeholder: true }} id="next-coupon-enter-code">
<input
className="w-full border rounded-md border-black/30 p-3 placeholder:text-grey-500 placeholder:font-normal focus:border focus:!border-black/30 focus:!shadow-[0_0_0_3px_rgba(10,132,255,0.3)] focus-visible:outline-none data-[invalid=true]:border-alert-red data-[invalid=true]:text-alert-red data-[invalid=true]:shadow-inputError"
type="text"
name="coupon"
data-testid="coupon-input"
placeholder="Enter code"
disabled={readOnly}
/>
</Localized>
<div>
<SubmitButton
className="w-20"
variant={ButtonVariant.Primary}
type="submit"
data-testid="coupon-button"
disabled={readOnly}
>
<Localized id="next-coupon-submit">Apply</Localized>
</SubmitButton>
</div>
</div>

{error && (
<div className="text-red-700 mt-4" data-testid="coupon-error">
<Localized id={error}>{getFallbackTextByFluentId(error)}</Localized>
</div>
)}
</form>
</>
);
};

interface CouponFormProps {
cartId: string;
cartVersion: number;
promoCode: string | null;
readOnly: boolean;
}

export function CouponForm({
cartId,
cartVersion,
promoCode,
readOnly,
}: CouponFormProps) {
const hasCouponCode = !!promoCode;
return (
<div className="bg-white rounded-b-lg shadow-sm shadow-grey-300 mt-6 p-4 rounded-t-lg text-base tablet:my-8">
{hasCouponCode ? (
<WithCoupon
cartId={cartId}
cartVersion={cartVersion}
couponCode={promoCode}
readOnly={readOnly}
/>
) : (
<WithoutCoupon
cartId={cartId}
cartVersion={cartVersion}
readOnly={readOnly}
/>
)}
</div>
);
}

export default CouponForm;
18 changes: 0 additions & 18 deletions libs/payments/ui/src/lib/client/components/PrimaryButton.tsx

This file was deleted.

Loading

0 comments on commit c33d786

Please sign in to comment.