From f5a232ecfb81082851253ce3fd2f69481c5ecc47 Mon Sep 17 00:00:00 2001 From: capJavert Date: Thu, 23 Jan 2025 15:03:58 +0100 Subject: [PATCH 1/5] feat: support plus unavailable --- .../src/components/plus/PlusDesktop.tsx | 8 +++-- .../src/components/plus/PlusUnavailable.tsx | 36 +++++++++++++++++++ .../shared/src/contexts/PaymentContext.tsx | 22 +++++++++--- packages/shared/src/lib/constants.ts | 12 +++++++ packages/shared/src/lib/featureManagement.ts | 4 +-- packages/webapp/pages/plus/payment.tsx | 8 +++-- 6 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 packages/shared/src/components/plus/PlusUnavailable.tsx diff --git a/packages/shared/src/components/plus/PlusDesktop.tsx b/packages/shared/src/components/plus/PlusDesktop.tsx index 42bce48f2b..519508e966 100644 --- a/packages/shared/src/components/plus/PlusDesktop.tsx +++ b/packages/shared/src/components/plus/PlusDesktop.tsx @@ -4,9 +4,11 @@ import { useRouter } from 'next/router'; import { usePaymentContext } from '../../contexts/PaymentContext'; import { PlusInfo } from './PlusInfo'; +import { PlusUnavailable } from './PlusUnavailable'; export const PlusDesktop = (): ReactElement => { - const { openCheckout, paddle, productOptions } = usePaymentContext(); + const { openCheckout, paddle, productOptions, isPlusAvailable } = + usePaymentContext(); const { query: { selectedPlan }, } = useRouter(); @@ -52,7 +54,9 @@ export const PlusDesktop = (): ReactElement => {
+ > + {!isPlusAvailable && } +
); }; diff --git a/packages/shared/src/components/plus/PlusUnavailable.tsx b/packages/shared/src/components/plus/PlusUnavailable.tsx new file mode 100644 index 0000000000..d29fc08673 --- /dev/null +++ b/packages/shared/src/components/plus/PlusUnavailable.tsx @@ -0,0 +1,36 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import classNames from 'classnames'; +import { IconSize } from '../Icon'; +import { SourceIcon } from '../icons'; +import { + TypographyType, + TypographyColor, + Typography, +} from '../typography/Typography'; + +export type PlusUnavailableProps = { + className?: string; +}; + +export const PlusUnavailable = ({ + className, +}: PlusUnavailableProps): ReactElement => { + return ( +
+ + + Unfortunately, this service is not available in your region. + +
+ ); +}; diff --git a/packages/shared/src/contexts/PaymentContext.tsx b/packages/shared/src/contexts/PaymentContext.tsx index 980534b413..33b0a5ed52 100644 --- a/packages/shared/src/contexts/PaymentContext.tsx +++ b/packages/shared/src/contexts/PaymentContext.tsx @@ -16,7 +16,7 @@ import { import { useQuery } from '@tanstack/react-query'; import { useRouter } from 'next/router'; import { useAuthContext } from './AuthContext'; -import { plusSuccessUrl } from '../lib/constants'; +import { invalidPlusRegions, plusSuccessUrl } from '../lib/constants'; import { LogEvent } from '../lib/log'; import { usePlusSubscription } from '../hooks'; import { logPixelPayment } from '../components/Pixels'; @@ -38,9 +38,10 @@ export interface PaymentContextData { paddle?: Paddle | undefined; productOptions?: ProductOption[]; earlyAdopterPlanId?: string | null; + isPlusAvailable: boolean; } -const PaymentContext = React.createContext({}); +const PaymentContext = React.createContext(undefined); export default PaymentContext; export type PaymentContextProviderProps = { @@ -58,6 +59,14 @@ export const PaymentContextProvider = ({ const logRef = useRef(); logRef.current = logSubscriptionEvent; + const isPlusAvailable = useMemo(() => { + if (!geo?.region) { + return false; + } + + return !invalidPlusRegions.includes(geo.region); + }, [geo?.region]); + // Download and initialize Paddle instance from CDN useEffect(() => { const existingPaddleInstance = getPaddleInstance(); @@ -120,6 +129,10 @@ export const PaymentContextProvider = ({ const openCheckout = useCallback( ({ priceId }: { priceId: string }) => { + if (!isPlusAvailable) { + return; + } + paddle?.Checkout.open({ items: [{ priceId, quantity: 1 }], customer: { @@ -139,7 +152,7 @@ export const PaymentContextProvider = ({ }, }); }, - [paddle, user], + [paddle, user, isPlusAvailable], ); const getPrices = useCallback(async () => { @@ -194,8 +207,9 @@ export const PaymentContextProvider = ({ paddle, productOptions, earlyAdopterPlanId, + isPlusAvailable, }), - [earlyAdopterPlanId, openCheckout, paddle, productOptions], + [earlyAdopterPlanId, openCheckout, paddle, productOptions, isPlusAvailable], ); return ( diff --git a/packages/shared/src/lib/constants.ts b/packages/shared/src/lib/constants.ts index 0053c191c5..1826b2dc58 100644 --- a/packages/shared/src/lib/constants.ts +++ b/packages/shared/src/lib/constants.ts @@ -127,3 +127,15 @@ export const feedRangeFilters: RadioItemProps[] = [ ]; export const customFeedsPlusDate = new Date('2024-12-11'); + +export const invalidPlusRegions = [ + 'CU', + 'IR', + 'MM', + 'SD', + 'SY', + 'KP', + 'BY', + 'ZW', + 'RU', +]; diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts index cca2db3fb0..f6acd85a99 100644 --- a/packages/shared/src/lib/featureManagement.ts +++ b/packages/shared/src/lib/featureManagement.ts @@ -34,8 +34,8 @@ const feature = { onboardingChecklist: new Feature('onboarding_checklist', true), showCodeSnippets: new Feature('show_code_snippets', false), pricingIds: new Feature('pricing_ids', { - pri_01jbsccbdbcwyhdy8hy3c2etyn: PlusPriceType.Monthly, - pri_01jbscda57910yvwjtyapnrrzc: PlusPriceType.Yearly, + pri_01jcdp5ef4yhv00p43hr2knrdg: PlusPriceType.Monthly, + pri_01jcdn6enr5ap3ekkddc6fv6tq: PlusPriceType.Yearly, }), }; diff --git a/packages/webapp/pages/plus/payment.tsx b/packages/webapp/pages/plus/payment.tsx index 5c2872d7f4..9d4fc9b3c3 100644 --- a/packages/webapp/pages/plus/payment.tsx +++ b/packages/webapp/pages/plus/payment.tsx @@ -6,11 +6,13 @@ import { useRouter } from 'next/router'; import { useViewSize, ViewSize } from '@dailydotdev/shared/src/hooks'; import { webappUrl } from '@dailydotdev/shared/src/lib/constants'; import { NextSeo } from 'next-seo'; +import { PlusUnavailable } from '@dailydotdev/shared/src/components/plus/PlusUnavailable'; + import { getPlusLayout } from '../../components/layouts/PlusLayout/PlusLayout'; const PlusPaymentPage = (): ReactElement => { const isLaptop = useViewSize(ViewSize.Laptop); - const { openCheckout } = usePaymentContext(); + const { openCheckout, isPlusAvailable } = usePaymentContext(); const router = useRouter(); const { pid } = router.query; @@ -36,7 +38,9 @@ const PlusPaymentPage = (): ReactElement => { openCheckout({ priceId: pid as string }); }} className="checkout-container h-full w-full bg-background-default p-5" - /> + > + {!isPlusAvailable && } + ); From 7b7cc063b0fa9b58b69a9cb8605ad23b8d738213 Mon Sep 17 00:00:00 2001 From: capJavert Date: Thu, 23 Jan 2025 15:17:35 +0100 Subject: [PATCH 2/5] fix: icon --- packages/shared/src/components/plus/PlusUnavailable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/components/plus/PlusUnavailable.tsx b/packages/shared/src/components/plus/PlusUnavailable.tsx index d29fc08673..03f9ad4b4e 100644 --- a/packages/shared/src/components/plus/PlusUnavailable.tsx +++ b/packages/shared/src/components/plus/PlusUnavailable.tsx @@ -2,7 +2,7 @@ import type { ReactElement } from 'react'; import React from 'react'; import classNames from 'classnames'; import { IconSize } from '../Icon'; -import { SourceIcon } from '../icons'; +import { SitesIcon } from '../icons'; import { TypographyType, TypographyColor, @@ -23,7 +23,7 @@ export const PlusUnavailable = ({ className, )} > - + Date: Thu, 23 Jan 2025 15:47:52 +0100 Subject: [PATCH 3/5] feat: adjust geo logic --- packages/shared/src/contexts/PaymentContext.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/shared/src/contexts/PaymentContext.tsx b/packages/shared/src/contexts/PaymentContext.tsx index 33b0a5ed52..2e2e538b51 100644 --- a/packages/shared/src/contexts/PaymentContext.tsx +++ b/packages/shared/src/contexts/PaymentContext.tsx @@ -60,11 +60,7 @@ export const PaymentContextProvider = ({ logRef.current = logSubscriptionEvent; const isPlusAvailable = useMemo(() => { - if (!geo?.region) { - return false; - } - - return !invalidPlusRegions.includes(geo.region); + return !invalidPlusRegions.includes(geo?.region); }, [geo?.region]); // Download and initialize Paddle instance from CDN From 4f54bb81685f19d046d1044c48a38d3d66419553 Mon Sep 17 00:00:00 2001 From: capJavert Date: Thu, 23 Jan 2025 21:41:52 +0100 Subject: [PATCH 4/5] revert pricing ids --- packages/shared/src/lib/featureManagement.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/lib/featureManagement.ts b/packages/shared/src/lib/featureManagement.ts index f6acd85a99..cca2db3fb0 100644 --- a/packages/shared/src/lib/featureManagement.ts +++ b/packages/shared/src/lib/featureManagement.ts @@ -34,8 +34,8 @@ const feature = { onboardingChecklist: new Feature('onboarding_checklist', true), showCodeSnippets: new Feature('show_code_snippets', false), pricingIds: new Feature('pricing_ids', { - pri_01jcdp5ef4yhv00p43hr2knrdg: PlusPriceType.Monthly, - pri_01jcdn6enr5ap3ekkddc6fv6tq: PlusPriceType.Yearly, + pri_01jbsccbdbcwyhdy8hy3c2etyn: PlusPriceType.Monthly, + pri_01jbscda57910yvwjtyapnrrzc: PlusPriceType.Yearly, }), }; From a96a4ecc1b78d46ec7399e70b1d28245a1ebfa81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ante=20Bari=C4=87?= Date: Fri, 24 Jan 2025 12:40:24 +0100 Subject: [PATCH 5/5] feat: user is plus already (#4104) --- .../components/plus/PlusCheckoutContainer.tsx | 51 ++++++++++++++++ .../src/components/plus/PlusDesktop.tsx | 19 +++--- .../shared/src/components/plus/PlusPlus.tsx | 58 +++++++++++++++++++ .../src/components/plus/PlusUnavailable.tsx | 5 +- .../shared/src/contexts/PaymentContext.tsx | 8 ++- packages/webapp/pages/plus/payment.tsx | 17 +++--- 6 files changed, 136 insertions(+), 22 deletions(-) create mode 100644 packages/shared/src/components/plus/PlusCheckoutContainer.tsx create mode 100644 packages/shared/src/components/plus/PlusPlus.tsx diff --git a/packages/shared/src/components/plus/PlusCheckoutContainer.tsx b/packages/shared/src/components/plus/PlusCheckoutContainer.tsx new file mode 100644 index 0000000000..a4fe7611cc --- /dev/null +++ b/packages/shared/src/components/plus/PlusCheckoutContainer.tsx @@ -0,0 +1,51 @@ +import type { ReactElement } from 'react'; +import React from 'react'; + +import classNames from 'classnames'; +import { usePaymentContext } from '../../contexts/PaymentContext'; +import { usePlusSubscription } from '../../hooks'; +import { PlusUnavailable } from './PlusUnavailable'; +import { PlusPlus } from './PlusPlus'; + +export type PlusCheckoutContainerProps = { + checkoutRef?: React.LegacyRef; + className?: { + container?: string; + element?: string; + }; +}; + +export const PlusCheckoutContainer = ({ + checkoutRef, + className, +}: PlusCheckoutContainerProps): ReactElement => { + const { isPlusAvailable } = usePaymentContext(); + const { isPlus } = usePlusSubscription(); + + const getContainerElement = () => { + if (!isPlusAvailable) { + return PlusUnavailable; + } + + if (isPlus) { + return PlusPlus; + } + + return null; + }; + + const ContainerElement = getContainerElement(); + const shouldRenderCheckout = !ContainerElement; + + return ( +
+ {ContainerElement && } +
+ ); +}; diff --git a/packages/shared/src/components/plus/PlusDesktop.tsx b/packages/shared/src/components/plus/PlusDesktop.tsx index 519508e966..191390fa6e 100644 --- a/packages/shared/src/components/plus/PlusDesktop.tsx +++ b/packages/shared/src/components/plus/PlusDesktop.tsx @@ -4,11 +4,10 @@ import { useRouter } from 'next/router'; import { usePaymentContext } from '../../contexts/PaymentContext'; import { PlusInfo } from './PlusInfo'; -import { PlusUnavailable } from './PlusUnavailable'; +import { PlusCheckoutContainer } from './PlusCheckoutContainer'; export const PlusDesktop = (): ReactElement => { - const { openCheckout, paddle, productOptions, isPlusAvailable } = - usePaymentContext(); + const { openCheckout, paddle, productOptions } = usePaymentContext(); const { query: { selectedPlan }, } = useRouter(); @@ -51,12 +50,14 @@ export const PlusDesktop = (): ReactElement => { onChange={toggleCheckoutOption} /> -
- {!isPlusAvailable && } -
+ ); }; diff --git a/packages/shared/src/components/plus/PlusPlus.tsx b/packages/shared/src/components/plus/PlusPlus.tsx new file mode 100644 index 0000000000..2ecd6a2eed --- /dev/null +++ b/packages/shared/src/components/plus/PlusPlus.tsx @@ -0,0 +1,58 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import classNames from 'classnames'; +import { + TypographyType, + TypographyColor, + Typography, +} from '../typography/Typography'; +import { PlusUser } from '../PlusUser'; +import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; +import { IconSize } from '../Icon'; +import { managePlusUrl } from '../../lib/constants'; +import type { WithClassNameProps } from '../utilities/common'; + +export type PlusPlusProps = WithClassNameProps; + +export const PlusPlus = ({ className }: PlusPlusProps): ReactElement => { + return ( +
+ +
+ + You are already a Plus member! + + + Thank you for supporting daily.dev and unlocking the best experience + we offer. Manage your subscription to update your plan, payment + details, or preferences anytime. + +
+ +
+ ); +}; diff --git a/packages/shared/src/components/plus/PlusUnavailable.tsx b/packages/shared/src/components/plus/PlusUnavailable.tsx index 03f9ad4b4e..d50d562f73 100644 --- a/packages/shared/src/components/plus/PlusUnavailable.tsx +++ b/packages/shared/src/components/plus/PlusUnavailable.tsx @@ -8,10 +8,9 @@ import { TypographyColor, Typography, } from '../typography/Typography'; +import type { WithClassNameProps } from '../utilities/common'; -export type PlusUnavailableProps = { - className?: string; -}; +export type PlusUnavailableProps = WithClassNameProps; export const PlusUnavailable = ({ className, diff --git a/packages/shared/src/contexts/PaymentContext.tsx b/packages/shared/src/contexts/PaymentContext.tsx index 2e2e538b51..adaddf443e 100644 --- a/packages/shared/src/contexts/PaymentContext.tsx +++ b/packages/shared/src/contexts/PaymentContext.tsx @@ -55,7 +55,7 @@ export const PaymentContextProvider = ({ const { user, geo } = useAuthContext(); const planTypes = useFeature(feature.pricingIds); const [paddle, setPaddle] = useState(); - const { logSubscriptionEvent } = usePlusSubscription(); + const { logSubscriptionEvent, isPlus } = usePlusSubscription(); const logRef = useRef(); logRef.current = logSubscriptionEvent; @@ -125,6 +125,10 @@ export const PaymentContextProvider = ({ const openCheckout = useCallback( ({ priceId }: { priceId: string }) => { + if (isPlus) { + return; + } + if (!isPlusAvailable) { return; } @@ -148,7 +152,7 @@ export const PaymentContextProvider = ({ }, }); }, - [paddle, user, isPlusAvailable], + [paddle, user, isPlusAvailable, isPlus], ); const getPrices = useCallback(async () => { diff --git a/packages/webapp/pages/plus/payment.tsx b/packages/webapp/pages/plus/payment.tsx index 9d4fc9b3c3..a37ef578e4 100644 --- a/packages/webapp/pages/plus/payment.tsx +++ b/packages/webapp/pages/plus/payment.tsx @@ -6,13 +6,13 @@ import { useRouter } from 'next/router'; import { useViewSize, ViewSize } from '@dailydotdev/shared/src/hooks'; import { webappUrl } from '@dailydotdev/shared/src/lib/constants'; import { NextSeo } from 'next-seo'; -import { PlusUnavailable } from '@dailydotdev/shared/src/components/plus/PlusUnavailable'; +import { PlusCheckoutContainer } from '@dailydotdev/shared/src/components/plus/PlusCheckoutContainer'; import { getPlusLayout } from '../../components/layouts/PlusLayout/PlusLayout'; const PlusPaymentPage = (): ReactElement => { const isLaptop = useViewSize(ViewSize.Laptop); - const { openCheckout, isPlusAvailable } = usePaymentContext(); + const { openCheckout } = usePaymentContext(); const router = useRouter(); const { pid } = router.query; @@ -29,18 +29,19 @@ const PlusPaymentPage = (): ReactElement => { <>
-
{ + { if (!element) { return; } openCheckout({ priceId: pid as string }); }} - className="checkout-container h-full w-full bg-background-default p-5" - > - {!isPlusAvailable && } -
+ className={{ + container: 'h-full w-full bg-background-default p-5', + element: 'h-full', + }} + />
);