From 8eee2205bf7a8361dfed8e080ca7acb2bb05a632 Mon Sep 17 00:00:00 2001 From: Zac Jones Date: Mon, 4 Nov 2024 17:03:22 -0700 Subject: [PATCH 1/2] feat: hide lifetime membership button --- .../active-sale.tsx} | 324 +++++++++--------- .../lifetime/lifetime-pricing-widget.tsx | 25 ++ .../pricing/lifetime/upcoming-sale.tsx | 120 +++++++ src/pages/forever/index.tsx | 5 +- src/utils/cio/subscriber-attributes.ts | 3 + 5 files changed, 307 insertions(+), 170 deletions(-) rename src/components/pricing/{lifetime-pricing-widget.tsx => lifetime/active-sale.tsx} (75%) create mode 100644 src/components/pricing/lifetime/lifetime-pricing-widget.tsx create mode 100644 src/components/pricing/lifetime/upcoming-sale.tsx create mode 100644 src/utils/cio/subscriber-attributes.ts diff --git a/src/components/pricing/lifetime-pricing-widget.tsx b/src/components/pricing/lifetime/active-sale.tsx similarity index 75% rename from src/components/pricing/lifetime-pricing-widget.tsx rename to src/components/pricing/lifetime/active-sale.tsx index ced89c9e7..951676478 100644 --- a/src/components/pricing/lifetime-pricing-widget.tsx +++ b/src/components/pricing/lifetime/active-sale.tsx @@ -1,24 +1,150 @@ -'use client' -import * as React from 'react' -import {FunctionComponent, useEffect} from 'react' -import {useViewer} from '@/context/viewer-context' -import {redirectToStandardCheckout} from '@/api/stripe/stripe-checkout-redirect' +import React from 'react' import emailIsValid from '@/utils/email-is-valid' -import {track} from '@/utils/analytics' -import {useRouter, useSearchParams} from 'next/navigation' -import PoweredByStripe from '@/components/pricing/powered-by-stripe' -import Spinner from '../spinner' +import {useRouter} from 'next/navigation' +import {useViewer} from '@/context/viewer-context' +import PoweredByStripe from '../powered-by-stripe' +import {twMerge} from 'tailwind-merge' import slugify from 'slugify' +import Spinner from '@/components/spinner' -// TODO: Extract PlanTitle and PlanPrice to shared components. +const ActiveSale = ({lastCharge}: {lastCharge: {amountPaid: number}}) => { + const {viewer} = useViewer() -const PlanTitle: React.FunctionComponent> = ({ - children, -}) => ( -

- {children} -

-) + const router = useRouter() + + const [loaderOn, setLoaderOn] = React.useState(false) + + const priceId = process.env.NEXT_PUBLIC_STRIPE_LIFETIME_MEMBERSHIP_PRICE_ID + const quantity = 1 + const pricesLoading = false + + const amountPaid = + lastCharge?.amountPaid === 0 ? null : lastCharge?.amountPaid + + const onClickCheckout = async () => { + if (!priceId) return + + if (emailIsValid(viewer?.email)) { + // Note: we don't want to do a `hasProAccess` check to abort the purchase. + // Instead, we let them through so that they can make the purchase because + // if they already have Pro, then this upgrades them to Lifetime Pro. + + // the user doesn't have pro access, proceed to checkout + + const {sessionUrl, error} = await fetch('/api/stripe/checkout/lifetime', { + method: 'POST', + body: JSON.stringify({ + email: viewer.email, + successPath: '/confirm/forever', + cancelPath: '/pricing/forever', + }), + }).then((res) => res.json()) + if (sessionUrl) { + router.push(sessionUrl) + } else { + console.error('error creating checkout session', error) + } + } else { + // const couponCode = state.context.couponToApply?.couponCode + + router.push( + '/forever/email?' + + new URLSearchParams({ + priceId, + quantity: quantity.toString(), + }), + ) + setLoaderOn(true) + } + } + + // TODO: make the priceId and display price come from env vars + // as an MVP, it is good enough for now to manually make sure those + // values match up with what is in Stripe. + const lifetimePlan = {price: 500, price_discounted: 250} + + return ( +
+
+
+ Lifetime Membership + {/* {!isPPP && appliedCoupon?.coupon_expires_at && !pricesLoading && ( + + )} */} +
+ +
+ {/* {!appliedCoupon && } + {quantityAvailable && ( +
+ { + onQuantityChanged(quantity) + }} + /> +
+ )} */} + + + +
+ {/* { + send({type: 'CONFIRM_PRICE', onClickCheckout}) + }} + quantityAvailable={true} + onQuantityChanged={(quantity: number) => { + send({type: 'CHANGE_QUANTITY', quantity}) + }} + onPriceChanged={(priceId: string) => { + send({type: 'SWITCH_PRICE', priceId}) + }} + currentPlan={currentPlan} + currentQuantity={quantity} + loaderOn={loaderOn} + appliedCoupon={appliedCoupon} + isPPP={pppCouponIsApplied} + /> */} +
+ {/* {pppCouponAvailable && pppCouponEligible && ( +
+ +
+ )} */} +
+ +
30 day money back guarantee
+
+
+ ) +} + +// TODO: Extract PlanTitle and PlanPrice to shared components. export const PlanPrice: React.FunctionComponent< React.PropsWithChildren<{ @@ -151,155 +277,17 @@ const PlanFeatures: React.FunctionComponent< ) } -const LifetimePricingWidget: FunctionComponent< - React.PropsWithChildren<{lastCharge: {amountPaid: number}}> -> = ({lastCharge}) => { - const {viewer, authToken} = useViewer() - - const router = useRouter() - const params = useSearchParams() - const stripeParam = params?.get('stripe') - - const [loaderOn, setLoaderOn] = React.useState(false) - - const priceId = process.env.NEXT_PUBLIC_STRIPE_LIFETIME_MEMBERSHIP_PRICE_ID - const quantity = 1 - const pricesLoading = false - - const amountPaid = - lastCharge?.amountPaid === 0 ? null : lastCharge?.amountPaid - - const onClickCheckout = async () => { - if (!priceId) return - track('lifetime checkout: selected plan', { - priceId: priceId, - }) - - if (emailIsValid(viewer?.email)) { - // Note: we don't want to do a `hasProAccess` check to abort the purchase. - // Instead, we let them through so that they can make the purchase because - // if they already have Pro, then this upgrades them to Lifetime Pro. - - // the user doesn't have pro access, proceed to checkout - track('lifetime checkout: valid email present', { - priceId: priceId, - }) - - const {sessionUrl, error} = await fetch('/api/stripe/checkout/lifetime', { - method: 'POST', - body: JSON.stringify({ - email: viewer.email, - successPath: '/confirm/forever', - cancelPath: '/pricing/forever', - }), - }).then((res) => res.json()) - if (sessionUrl) { - router.push(sessionUrl) - } else { - console.error('error creating checkout session', error) - } - } else { - track('checkout: get email', { - priceId: priceId, - }) - - // const couponCode = state.context.couponToApply?.couponCode - - router.push( - '/forever/email?' + - new URLSearchParams({ - priceId, - quantity: quantity.toString(), - }), - ) - setLoaderOn(true) - } - } - - // TODO: make the priceId and display price come from env vars - // as an MVP, it is good enough for now to manually make sure those - // values match up with what is in Stripe. - const lifetimePlan = {price: 500, price_discounted: 250} - - return ( -
-
-
- Lifetime Membership - {/* {!isPPP && appliedCoupon?.coupon_expires_at && !pricesLoading && ( - - )} */} -
- -
- {/* {!appliedCoupon && } - {quantityAvailable && ( -
- { - onQuantityChanged(quantity) - }} - /> -
- )} */} - - - -
- {/* { - send({type: 'CONFIRM_PRICE', onClickCheckout}) - }} - quantityAvailable={true} - onQuantityChanged={(quantity: number) => { - send({type: 'CHANGE_QUANTITY', quantity}) - }} - onPriceChanged={(priceId: string) => { - send({type: 'SWITCH_PRICE', priceId}) - }} - currentPlan={currentPlan} - currentQuantity={quantity} - loaderOn={loaderOn} - appliedCoupon={appliedCoupon} - isPPP={pppCouponIsApplied} - /> */} -
- {/* {pppCouponAvailable && pppCouponEligible && ( -
- -
- )} */} -
- -
30 day money back guarantee
-
-
- ) -} +export const PlanTitle: React.FunctionComponent< + React.PropsWithChildren<{className?: string}> +> = ({children, className}) => ( +

+ {children} +

+) -export default LifetimePricingWidget +export default ActiveSale diff --git a/src/components/pricing/lifetime/lifetime-pricing-widget.tsx b/src/components/pricing/lifetime/lifetime-pricing-widget.tsx new file mode 100644 index 000000000..74f0ea0a0 --- /dev/null +++ b/src/components/pricing/lifetime/lifetime-pricing-widget.tsx @@ -0,0 +1,25 @@ +'use client' +import * as React from 'react' +import {FunctionComponent} from 'react' +import {useSearchParams} from 'next/navigation' +import ActiveSale from './active-sale' +import UpcomingSale from './upcoming-sale' + +const LifetimePricingWidget: FunctionComponent< + React.PropsWithChildren<{lastCharge: {amountPaid: number}}> +> = ({lastCharge}) => { + const searchParams = useSearchParams() + const allowPurchase = searchParams?.get('allowPurchase') ?? false + + return ( + <> + {allowPurchase === 'true' ? ( + + ) : ( + + )} + + ) +} + +export default LifetimePricingWidget diff --git a/src/components/pricing/lifetime/upcoming-sale.tsx b/src/components/pricing/lifetime/upcoming-sale.tsx new file mode 100644 index 000000000..6acf1a973 --- /dev/null +++ b/src/components/pricing/lifetime/upcoming-sale.tsx @@ -0,0 +1,120 @@ +import * as React from 'react' +import {useViewer} from '@/context/viewer-context' +import emailIsValid from '@/utils/email-is-valid' +import PoweredByStripe from '@/components/pricing/powered-by-stripe' +import useCio from '@/hooks/use-cio' +import {INTERESTED_IN_LIFETIME_SINCE} from '@/utils/cio/subscriber-attributes' +import {requestSignInEmail} from '@/utils/request-signin-email' +import {cx} from 'class-variance-authority' +import {CheckCircleIcon} from '@heroicons/react/solid' +import {PlanTitle} from './active-sale' + +const UpcomingSale = () => { + const {viewer} = useViewer() + const {subscriber, cioIdentify} = useCio() + + const [email, setEmail] = React.useState('') + const [signedUp, setSignedUp] = React.useState(false) + + async function handleClick(submittedEmail?: string) { + setSignedUp(true) + + // there will only be a submittedEmail if there is no viewer. + // subscriber can still exist because it looks at cookies in the browser but would be confusing for the user if a different email was used than the one they submitted + if (submittedEmail) { + const {contact_id} = await requestSignInEmail(submittedEmail) + + cioIdentify(contact_id, { + email: submittedEmail, + [INTERESTED_IN_LIFETIME_SINCE]: Math.floor(Date.now() / 1000), + }) + } else if (subscriber) { + cioIdentify(subscriber.id, { + [INTERESTED_IN_LIFETIME_SINCE]: Math.floor(Date.now() / 1000), + }) + } + } + + return ( +
+
+
+ Lifetime Membership +
+

+ {' '} + The sale for lifetime memberships is currently closed.{' '} +

+

+ If you'd like to receive updates when the sale opens up again, + sign up below. +

+
+ +
+ {viewer ? ( + + ) : ( +
+ + setEmail(event.target.value)} + /> + +
+ )} +
+
+

+ You've + been added to the list! We'll notify you when the sale opens up + again. +

+
+
+
+
+ +
30 day money back guarantee
+
+
+ ) +} + +export default UpcomingSale diff --git a/src/pages/forever/index.tsx b/src/pages/forever/index.tsx index 94f2626f9..5c919d4e6 100644 --- a/src/pages/forever/index.tsx +++ b/src/pages/forever/index.tsx @@ -4,7 +4,7 @@ import {track} from '@/utils/analytics' import {useRouter} from 'next/router' import Testimonials from '@/components/pricing/testimonials' import testimonialsData from '@/components/pricing/testimonials/data' -import LifetimePricingWidget from '@/components/pricing/lifetime-pricing-widget' +import LifetimePricingWidget from '@/components/pricing/lifetime/lifetime-pricing-widget' import Layout from '@/components/app/layout' import {NextSeo} from 'next-seo' import {GetServerSideProps} from 'next' @@ -48,7 +48,8 @@ const Forever: FunctionComponent> & { egghead lifetime membership

- Stay Current with Modern Full-Stack Courses for Professional Web Developers + Stay Current with Modern Full-Stack Courses for Professional Web + Developers

Learn the skills you and your team need to build real-world business diff --git a/src/utils/cio/subscriber-attributes.ts b/src/utils/cio/subscriber-attributes.ts new file mode 100644 index 000000000..12edf74ec --- /dev/null +++ b/src/utils/cio/subscriber-attributes.ts @@ -0,0 +1,3 @@ +const INTERESTED_IN_LIFETIME_SINCE = 'interested_in_lifetime_since' + +export {INTERESTED_IN_LIFETIME_SINCE} From 7fc28f8bc19f9578358c2c28d70c97899dd3d5bd Mon Sep 17 00:00:00 2001 From: Zac Jones Date: Mon, 4 Nov 2024 17:05:52 -0700 Subject: [PATCH 2/2] fix: remove stripe badge from UpcomingSale --- src/components/pricing/lifetime/upcoming-sale.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/pricing/lifetime/upcoming-sale.tsx b/src/components/pricing/lifetime/upcoming-sale.tsx index 6acf1a973..00c9cb1ef 100644 --- a/src/components/pricing/lifetime/upcoming-sale.tsx +++ b/src/components/pricing/lifetime/upcoming-sale.tsx @@ -109,10 +109,6 @@ const UpcomingSale = () => { -
- -
30 day money back guarantee
-
) }