-
Notifications
You must be signed in to change notification settings - Fork 13.5k
feat: add Paystack payment integration #29296
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
Open
MarvelNwachukwu
wants to merge
29
commits into
calcom:main
Choose a base branch
from
MarvelNwachukwu:feat/paystack-upstream
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
cf5507f
feat: add Paystack payment integration
MarvelNwachukwu acacbb9
fix: address code review issues in Paystack integration
MarvelNwachukwu 72e0641
fix: rollback payment lock on any post-lock failure in webhook
MarvelNwachukwu 3409a1f
Merge branch 'main' into feat/paystack-upstream
MarvelNwachukwu a24f8bb
Merge branch 'main' into feat/paystack-upstream
MarvelNwachukwu 441e7e0
fix(paystack): address review feedback
MarvelNwachukwu ba3b9d7
fix(paystack): drop credential.key from setup props and validate teamId
MarvelNwachukwu 08aad53
fix(paystack): make credential install atomic and drop string-sentinel
MarvelNwachukwu a5e9baa
fix(paystack): show AppNotInstalledMessage when credentialId is missing
MarvelNwachukwu d4030cb
fix(paystack): add fetch timeouts and check response.ok before parsing
MarvelNwachukwu d609985
fix(paystack): atomic idempotency lock in verify; validate reference
MarvelNwachukwu e34ff5f
fix(paystack): silence success sentinel in outer catch; ack unknown refs
MarvelNwachukwu c9f5b01
fix(paystack): log deletePayment failures; guard Intl.NumberFormat
MarvelNwachukwu 3695889
fix(paystack): drop manual edits to autogenerated app-store files
MarvelNwachukwu 7aeec84
chore(paystack): pick up routing-forms entry from app-store generator
MarvelNwachukwu fd2ca9a
chore: skip biome on .d.ts files in lint-staged
MarvelNwachukwu ed2e7ee
fix(paystack): add ambient declaration for @paystack/inline-js
MarvelNwachukwu cf7ce0b
Merge remote-tracking branch 'upstream/main' into feat/paystack-upstream
MarvelNwachukwu 6f9c4c5
fix(paystack): drop role="presentation" from setup inputs
MarvelNwachukwu 68411fe
fix(paystack): migrate PaymentService to ErrorWithCode
MarvelNwachukwu 822f86d
fix(paystack): simplify PaymentService and tighten typing
MarvelNwachukwu b4df242
fix(paystack): validate payment.data via zod wrapper; drop unsafe cast
MarvelNwachukwu 688a6e2
fix(paystack): encode reference param when calling verify endpoint
MarvelNwachukwu 6fc211c
fix(paystack): tighten teamId parsing; templatize brand in i18n string
MarvelNwachukwu 4a6db23
chore(paystack): swap custom inline-js.d.ts for @types/paystack__inli…
MarvelNwachukwu 4f3ed11
docs(paystack): expand HttpCode(200) success-sentinel rationale in ve…
MarvelNwachukwu b2caa6a
fix(paystack): fail closed when credential identifier is missing
MarvelNwachukwu a6217d2
fix(paystack): fire-and-forget verify call so a stalled endpoint can'…
MarvelNwachukwu bd25254
chore(paystack): format currency in the viewer's locale
MarvelNwachukwu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
apps/web/components/apps/paystack/PaystackPaymentComponent.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| "use client"; | ||
|
|
||
| import dynamic from "next/dynamic"; | ||
| import z from "zod"; | ||
|
|
||
| import { useLocale } from "@calcom/lib/hooks/useLocale"; | ||
|
|
||
| const PaystackInlineComponent = dynamic( | ||
| () => import("@calcom/app-store/paystack/components/PaystackPaymentComponent"), | ||
| { ssr: false } | ||
| ); | ||
|
|
||
| const PaystackPaymentDataSchema = z.object({ | ||
| access_code: z.string().min(1), | ||
| authorization_url: z.string().url(), | ||
| publicKey: z.string().min(1), | ||
| reference: z.string().min(1), | ||
| }); | ||
|
|
||
| type Props = { | ||
| payment: { data: unknown }; | ||
| bookingUid: string; | ||
| bookingTitle: string; | ||
| amount: number; | ||
| currency: string; | ||
| }; | ||
|
|
||
| export const PaystackPaymentComponent = ({ | ||
| payment, | ||
| bookingUid, | ||
| bookingTitle, | ||
| amount, | ||
| currency, | ||
| }: Props) => { | ||
| const { t } = useLocale(); | ||
| const parsed = PaystackPaymentDataSchema.safeParse(payment.data); | ||
|
|
||
| if (!parsed.success) { | ||
| return <p className="mt-3 text-center">{t("payment_failed_try_again")}</p>; | ||
| } | ||
|
|
||
| return ( | ||
| <PaystackInlineComponent | ||
| payment={{ data: parsed.data }} | ||
| bookingUid={bookingUid} | ||
| bookingTitle={bookingTitle} | ||
| amount={amount} | ||
| currency={currency} | ||
| /> | ||
| ); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| import { useRouter } from "next/navigation"; | ||
| import { useState } from "react"; | ||
| import { Toaster } from "sonner"; | ||
|
|
||
| import AppNotInstalledMessage from "@calcom/app-store/_components/AppNotInstalledMessage"; | ||
| import { APP_NAME } from "@calcom/lib/constants"; | ||
| import { useLocale } from "@calcom/lib/hooks/useLocale"; | ||
| import { trpc } from "@calcom/trpc/react"; | ||
| import { Button } from "@calcom/ui/components/button"; | ||
| import { TextField } from "@calcom/ui/components/form"; | ||
| import { showToast } from "@calcom/ui/components/toast"; | ||
|
|
||
| export default function PaystackSetup() { | ||
| const [newPublicKey, setNewPublicKey] = useState(""); | ||
| const [newSecretKey, setNewSecretKey] = useState(""); | ||
| const router = useRouter(); | ||
| const { t } = useLocale(); | ||
|
|
||
| const integrations = trpc.viewer.apps.integrations.useQuery({ | ||
| variant: "payment", | ||
| appId: "paystack", | ||
| }); | ||
|
|
||
| const [paystackCredentials] = integrations.data?.items || []; | ||
| const credentialId = paystackCredentials?.userCredentialIds?.[0]; | ||
|
|
||
| const showContent = | ||
| !!integrations.data && integrations.isSuccess && typeof credentialId === "number" && credentialId > 0; | ||
|
|
||
| const saveKeysMutation = trpc.viewer.apps.updateAppCredentials.useMutation({ | ||
| onSuccess: () => { | ||
| showToast(t("keys_have_been_saved"), "success"); | ||
| router.push("/event-types"); | ||
| }, | ||
| onError: (error) => { | ||
| showToast(error.message, "error"); | ||
| }, | ||
| }); | ||
|
|
||
| if (integrations.isPending) { | ||
| return <div className="absolute z-50 flex h-screen w-full items-center bg-gray-200" />; | ||
| } | ||
|
|
||
| return ( | ||
| <div className="bg-default flex h-screen"> | ||
| {showContent ? ( | ||
| <div className="bg-default border-subtle m-auto max-w-[43em] overflow-auto rounded border pb-10 md:p-10"> | ||
| <div className="ml-2 ltr:mr-2 rtl:ml-2 md:ml-5"> | ||
| <div className="invisible md:visible"> | ||
| <img className="h-11" src="/api/app-store/paystack/icon.svg" alt="Paystack" /> | ||
| <p className="text-default mt-5 text-lg">Paystack</p> | ||
| </div> | ||
|
|
||
| <form | ||
| autoComplete="off" | ||
| className="mt-5" | ||
| onSubmit={(e) => { | ||
| e.preventDefault(); | ||
| if (typeof credentialId !== "number") return; | ||
| saveKeysMutation.mutate({ | ||
| credentialId, | ||
| key: { | ||
| public_key: newPublicKey, | ||
| secret_key: newSecretKey, | ||
| }, | ||
| }); | ||
| }}> | ||
| <TextField | ||
| label={t("paystack_public_key")} | ||
| type="text" | ||
| name="public_key" | ||
| id="public_key" | ||
| value={newPublicKey} | ||
| onChange={(e) => setNewPublicKey(e.target.value)} | ||
| className="mb-6" | ||
| placeholder="pk_test_xxxxxxxxx" | ||
| /> | ||
|
|
||
| <TextField | ||
| label={t("paystack_secret_key")} | ||
| type="password" | ||
| name="secret_key" | ||
| id="secret_key" | ||
| value={newSecretKey} | ||
| autoComplete="new-password" | ||
| onChange={(e) => setNewSecretKey(e.target.value)} | ||
| placeholder="sk_test_xxxxxxxxx" | ||
| /> | ||
|
|
||
| <div className="mt-5 flex flex-row justify-end"> | ||
| <Button | ||
| type="submit" | ||
| color="primary" | ||
| loading={saveKeysMutation.isPending} | ||
| disabled={!newPublicKey || !newSecretKey}> | ||
| {t("save")} | ||
| </Button> | ||
| </div> | ||
| </form> | ||
|
|
||
| <div className="mt-5"> | ||
| <p className="text-default font-bold">{t("getting_started")}</p> | ||
| <p className="text-default mt-2"> | ||
| {t("paystack_getting_started_description", { appName: APP_NAME })}{" "} | ||
| <a | ||
| className="text-blue-600 underline" | ||
| target="_blank" | ||
| href="https://dashboard.paystack.com/#/settings/developers" | ||
| rel="noreferrer"> | ||
| {t("paystack_dashboard")} | ||
| </a> | ||
| . | ||
| </p> | ||
|
|
||
| <p className="text-default mt-4 font-bold">{t("paystack_webhook_setup")}</p> | ||
| <p className="text-default mt-2"> | ||
| {t("paystack_webhook_setup_description")} | ||
| </p> | ||
| <code className="bg-subtle mt-2 block rounded p-2 text-sm"> | ||
| {typeof window !== "undefined" ? window.location.origin : "https://your-cal.com"} | ||
| /api/integrations/paystack/webhook | ||
| </code> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ) : ( | ||
| <AppNotInstalledMessage appName="paystack" /> | ||
| )} | ||
|
|
||
| <Toaster position="bottom-right" /> | ||
| </div> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,11 @@ | ||
| const quotePath = (file) => `"${file.replace(/"/g, '\\"')}"`; | ||
|
|
||
| export default { | ||
| "(apps|packages|companion)/**/*.{js,ts,jsx,tsx}": (files) => | ||
| `biome lint --reporter summary --config-path=biome-staged.json ${files.map(quotePath).join(" ")}`, | ||
| "(apps|packages|companion)/**/*.{js,ts,jsx,tsx}": (files) => { | ||
| // biome.json ignores **/*.d.ts; passing them in errors the run. | ||
| const lintable = files.filter((f) => !f.endsWith(".d.ts")); | ||
| if (lintable.length === 0) return []; | ||
| return `biome lint --reporter summary --config-path=biome-staged.json ${lintable.map(quotePath).join(" ")}`; | ||
| }, | ||
| "packages/prisma/schema.prisma": ["prisma format"], | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import type { AppMeta } from "@calcom/types/App"; | ||
|
|
||
| import _package from "./package.json"; | ||
|
|
||
| export const metadata = { | ||
| name: "Paystack", | ||
| description: _package.description, | ||
| installed: true, | ||
| type: "paystack_payment", | ||
| variant: "payment", | ||
| logo: "icon.svg", | ||
| publisher: "Cal.com", | ||
| url: "https://paystack.com", | ||
| categories: ["payment"], | ||
| slug: "paystack", | ||
| title: "Paystack", | ||
| email: "support@cal.com", | ||
| dirName: "paystack", | ||
| isOAuth: true, | ||
| } as AppMeta; | ||
|
|
||
| export default metadata; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.