From 02425702d1ebd2a68856069d376619cb3a81319f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Wed, 21 Dec 2022 13:31:09 -0700 Subject: [PATCH] Fixes/stripe improvements (#5698) * Enables require confirmation after payment its made * Various Stripe improvements * Use constant * Merge branch 'main' into fixes/stripe-improvements * Update getStripeAppKeys.ts * submodule sync * Submodule sync * Revert * Update apps/web/components/booking/BookingListItem.tsx Co-authored-by: alannnc Co-authored-by: Alan Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/app-store/_components/AppCard.tsx | 8 ++++- .../_components/OmniInstallAppButton.tsx | 11 ++++++- .../app-store/_utils/useAddAppMutation.ts | 13 +++++---- packages/app-store/package.json | 3 +- packages/app-store/stripepayment/api/add.ts | 29 ++++++++++++------- .../app-store/stripepayment/api/callback.ts | 13 ++++++++- .../stripepayment/api/subscription.ts | 3 +- .../extensions/EventTypeAppCard.tsx | 4 +++ .../lib/client/createPaymentLink.ts | 4 ++- .../stripepayment/lib/getStripeAppKeys.ts | 4 +++ .../app-store/stripepayment/lib/server.ts | 1 + yarn.lock | 5 ++++ 12 files changed, 77 insertions(+), 21 deletions(-) create mode 100644 packages/app-store/stripepayment/lib/getStripeAppKeys.ts diff --git a/packages/app-store/_components/AppCard.tsx b/packages/app-store/_components/AppCard.tsx index 302fb1ba63..5ca6c5998d 100644 --- a/packages/app-store/_components/AppCard.tsx +++ b/packages/app-store/_components/AppCard.tsx @@ -15,6 +15,7 @@ export default function AppCard({ switchChecked, children, setAppData, + returnTo, }: { app: RouterOutputs["viewer"]["apps"][number]; description?: React.ReactNode; @@ -22,6 +23,7 @@ export default function AppCard({ switchOnClick?: (e: boolean) => void; children?: React.ReactNode; setAppData: SetAppDataGeneric; + returnTo?: string; }) { const [animationRef] = useAutoAnimate(); @@ -55,7 +57,11 @@ export default function AppCard({ /> ) : ( - + )} diff --git a/packages/app-store/_components/OmniInstallAppButton.tsx b/packages/app-store/_components/OmniInstallAppButton.tsx index 3ed9f2243f..b3645db335 100644 --- a/packages/app-store/_components/OmniInstallAppButton.tsx +++ b/packages/app-store/_components/OmniInstallAppButton.tsx @@ -11,12 +11,21 @@ import { InstallAppButton } from "../components"; * Use this component to allow installing an app from anywhere on the app. * Use of this component requires you to remove custom InstallAppButtonComponent so that it can manage the redirection itself */ -export default function OmniInstallAppButton({ appId, className }: { appId: string; className: string }) { +export default function OmniInstallAppButton({ + appId, + className, + returnTo, +}: { + appId: string; + className: string; + returnTo?: string; +}) { const { t } = useLocale(); const { data: app } = useApp(appId); const utils = trpc.useContext(); const mutation = useAddAppMutation(null, { + returnTo, onSuccess: (data) => { //TODO: viewer.appById might be replaced with viewer.apps so that a single query needs to be invalidated. utils.viewer.appById.invalidate({ appId }); diff --git a/packages/app-store/_utils/useAddAppMutation.ts b/packages/app-store/_utils/useAddAppMutation.ts index e7a5c52027..3d5836bf2c 100644 --- a/packages/app-store/_utils/useAddAppMutation.ts +++ b/packages/app-store/_utils/useAddAppMutation.ts @@ -21,9 +21,11 @@ type CustomUseMutationOptions = type AddAppMutationData = { setupPending: boolean } | void; type UseAddAppMutationOptions = CustomUseMutationOptions & { onSuccess: (data: AddAppMutationData) => void; + returnTo?: string; }; -function useAddAppMutation(_type: App["type"] | null, options?: UseAddAppMutationOptions) { +function useAddAppMutation(_type: App["type"] | null, allOptions?: UseAddAppMutationOptions) { + const { returnTo, ...options } = allOptions || {}; const mutation = useMutation< AddAppMutationData, Error, @@ -42,11 +44,12 @@ function useAddAppMutation(_type: App["type"] | null, options?: UseAddAppMutatio } const state: IntegrationOAuthCallbackState = { returnTo: + returnTo || WEBAPP_URL + - getInstalledAppPath( - { variant: variables && variables.variant, slug: variables && variables.slug }, - location.search - ), + getInstalledAppPath( + { variant: variables && variables.variant, slug: variables && variables.slug }, + location.search + ), }; const stateStr = encodeURIComponent(JSON.stringify(state)); const searchParams = `?state=${stateStr}`; diff --git a/packages/app-store/package.json b/packages/app-store/package.json index 964eba90c4..f5e8cff00e 100644 --- a/packages/app-store/package.json +++ b/packages/app-store/package.json @@ -20,7 +20,8 @@ "@calcom/trpc": "*", "@calcom/ui": "*", "@calcom/zoomvideo": "*", - "lodash": "^4.17.21" + "lodash": "^4.17.21", + "qs-stringify": "^1.2.1" }, "devDependencies": { "@calcom/types": "*" diff --git a/packages/app-store/stripepayment/api/add.ts b/packages/app-store/stripepayment/api/add.ts index c6124da63c..1f82f1e4f1 100644 --- a/packages/app-store/stripepayment/api/add.ts +++ b/packages/app-store/stripepayment/api/add.ts @@ -1,12 +1,16 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import { stringify } from "querystring"; +import stringify from "qs-stringify"; +import Stripe from "stripe"; +import { z } from "zod"; -import { BASE_URL } from "@calcom/lib/constants"; +import { WEBAPP_URL } from "@calcom/lib/constants"; import prisma from "@calcom/prisma"; -const client_id = process.env.STRIPE_CLIENT_ID; +import { getStripeAppKeys } from "../lib/getStripeAppKeys"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { client_id } = await getStripeAppKeys(); + if (req.method === "GET") { // Get user const user = await prisma.user.findUnique({ @@ -19,18 +23,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, }); - const redirect_uri = encodeURI(BASE_URL + "/api/integrations/stripepayment/callback"); - const stripeConnectParams = { + const redirect_uri = encodeURI(WEBAPP_URL + "/api/integrations/stripepayment/callback"); + const stripeConnectParams: Stripe.OAuthAuthorizeUrlParams = { client_id, scope: "read_write", response_type: "code", - "stripe_user[email]": user?.email, - "stripe_user[first_name]": user?.name, - /** We need this so E2E don't fail for international users */ - "stripe_user[country]": process.env.NEXT_PUBLIC_IS_E2E ? "US" : undefined, + stripe_user: { + email: user?.email, + first_name: user?.name || undefined, + /** We need this so E2E don't fail for international users */ + country: process.env.NEXT_PUBLIC_IS_E2E ? "US" : undefined, + }, redirect_uri, + state: typeof req.query.state === "string" ? req.query.state : undefined, }; - const query = stringify(stripeConnectParams); + /** stringify is being dumb here */ + const params = z.record(z.any()).parse(stripeConnectParams); + const query = stringify(params); /** * Choose Express or Standard Stripe accounts * @url https://stripe.com/docs/connect/accounts diff --git a/packages/app-store/stripepayment/api/callback.ts b/packages/app-store/stripepayment/api/callback.ts index bb31f8c89a..bc2b1d5b53 100644 --- a/packages/app-store/stripepayment/api/callback.ts +++ b/packages/app-store/stripepayment/api/callback.ts @@ -7,6 +7,16 @@ import prisma from "@calcom/prisma"; import getInstalledAppPath from "../../_utils/getInstalledAppPath"; import stripe, { StripeData } from "../lib/server"; +function getReturnToValueFromQueryState(req: NextApiRequest) { + let returnTo = ""; + try { + returnTo = JSON.parse(`${req.query.state}`).returnTo; + } catch (error) { + console.info("No 'returnTo' in req.query.state"); + } + return returnTo; +} + export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { code, error, error_description } = req.query; @@ -40,5 +50,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, }); - res.redirect(getInstalledAppPath({ variant: "payment", slug: "stripe" })); + const returnTo = getReturnToValueFromQueryState(req); + res.redirect(returnTo || getInstalledAppPath({ variant: "payment", slug: "stripe" })); } diff --git a/packages/app-store/stripepayment/api/subscription.ts b/packages/app-store/stripepayment/api/subscription.ts index e3f9194090..857d8066c4 100644 --- a/packages/app-store/stripepayment/api/subscription.ts +++ b/packages/app-store/stripepayment/api/subscription.ts @@ -3,6 +3,7 @@ import Stripe from "stripe"; import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils"; import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername"; +import { WEBAPP_URL } from "@calcom/lib/constants"; import prisma from "@calcom/prisma"; import { Prisma } from "@calcom/prisma/client"; @@ -36,7 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return; } - const return_url = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/integrations/stripepayment/paymentCallback?checkoutSessionId={CHECKOUT_SESSION_ID}&callbackUrl=${callbackUrl}`; + const return_url = `${WEBAPP_URL}/api/integrations/stripepayment/paymentCallback?checkoutSessionId={CHECKOUT_SESSION_ID}&callbackUrl=${callbackUrl}`; const createSessionParams: Stripe.Checkout.SessionCreateParams = { mode: "subscription", payment_method_types: ["card"], diff --git a/packages/app-store/stripepayment/extensions/EventTypeAppCard.tsx b/packages/app-store/stripepayment/extensions/EventTypeAppCard.tsx index b77faeb0ec..a90adf2457 100644 --- a/packages/app-store/stripepayment/extensions/EventTypeAppCard.tsx +++ b/packages/app-store/stripepayment/extensions/EventTypeAppCard.tsx @@ -1,15 +1,18 @@ +import { useRouter } from "next/router"; import { useState } from "react"; import { FormattedNumber, IntlProvider } from "react-intl"; import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import AppCard from "@calcom/app-store/_components/AppCard"; import type { EventTypeAppCardComponent } from "@calcom/app-store/types"; +import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Alert, TextField } from "@calcom/ui"; import { appDataSchema } from "../zod"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { + const { asPath } = useRouter(); const [getAppData, setAppData] = useAppContextWithSchema(); const price = getAppData("price"); const currency = getAppData("currency"); @@ -28,6 +31,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ .trim(); return ( = T | undefined | null; export function createPaymentLink(opts: { @@ -11,7 +13,7 @@ export function createPaymentLink(opts: { }): string { const { paymentUid, name, email, date, absolute = true } = opts; let link = ""; - if (absolute) link = process.env.NEXT_PUBLIC_WEBSITE_URL!; + if (absolute) link = WEBSITE_URL; const query = stringify({ date, name, email }); return link + `/payment/${paymentUid}?${query}`; } diff --git a/packages/app-store/stripepayment/lib/getStripeAppKeys.ts b/packages/app-store/stripepayment/lib/getStripeAppKeys.ts new file mode 100644 index 0000000000..c3f795f602 --- /dev/null +++ b/packages/app-store/stripepayment/lib/getStripeAppKeys.ts @@ -0,0 +1,4 @@ +import getParsedAppKeysFromSlug from "../../_utils/getParsedAppKeysFromSlug"; +import { appKeysSchema } from "../zod"; + +export const getStripeAppKeys = () => getParsedAppKeysFromSlug("stripe", appKeysSchema); diff --git a/packages/app-store/stripepayment/lib/server.ts b/packages/app-store/stripepayment/lib/server.ts index bdd1a7829f..e937c180f3 100644 --- a/packages/app-store/stripepayment/lib/server.ts +++ b/packages/app-store/stripepayment/lib/server.ts @@ -34,6 +34,7 @@ export const stripeDataSchema = stripeOAuthTokenSchema.extend({ export type StripeData = z.infer; +/** Figure out a way to get this from the DB without too much wreckage. */ const stripePrivateKey = process.env.STRIPE_PRIVATE_KEY!; const stripe = new Stripe(stripePrivateKey, { apiVersion: "2020-08-27", diff --git a/yarn.lock b/yarn.lock index 6b5eac8474..41d8c91590 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21971,6 +21971,11 @@ qrcode@^1.5.1: pngjs "^5.0.0" yargs "^15.3.1" +qs-stringify@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/qs-stringify/-/qs-stringify-1.2.1.tgz#9b39ef6b816bd83309628fc9dad435fc0eccc28b" + integrity sha512-2N5xGLGZUxpgAYq1fD1LmBSCbxQVsXYt5JU0nU3FuPWO8PlCnKNFQwXkZgyB6mrTdg7IbexX4wxIR403dJw9pw== + qs@6.10.3, qs@^6.10.2, qs@^6.10.3, qs@^6.7.0, qs@^6.9.4: version "6.10.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"