├── .env.example ├── .gitignore ├── LICENSE.md ├── README.md ├── components └── Form.tsx ├── constants └── addresses.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── api │ ├── stripe_intent.ts │ └── webhook.ts └── index.tsx ├── public ├── favicon.ico └── thirdweb.svg ├── readme.md ├── styles ├── Home.module.css └── globals.css └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_MAGIC_LINK_API_KEY= 2 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= 3 | STRIPE_SECRET_KEY= 4 | WEBHOOK_SECRET_KEY= 5 | PRIVATE_KEY= 6 | NEXT_PUBLIC_DOMAIN= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Non-Fungible Labs, Inc 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!Important] 2 | > This repository is referencing the `mumbai` chain. 3 | > 4 | > `Mumbai` [is deprecated since 08/04/2024](https://blog.thirdweb.com/deprecation-of-mumbai-testnet/), meaning the code in this repository will no longer work out of the box. 5 | > 6 | > You can still use this repository, however you will have to switch any references to `mumbai` to another chain. 7 | 8 | # Seamless NFT Experience 9 | 10 | This project demonstrates how you can create a seamless NFT experience for your users. 11 | 12 | We use Magic.link to authenticate users using their email, then we use stripe to collect payment for the NFTs and send them to the user's wallet generated by Magic. 13 | 14 | ## Using This Template 15 | 16 | Create a project using this example: 17 | 18 | ```bash 19 | npx thirdweb create --template seamless-stripe-checkout 20 | ``` 21 | 22 | - Create an [Edition](https://thirdweb.com/thirdweb.eth/TokenERC1155) contract using the dashboard. 23 | - Add an NFT to the token contract with your desired metadata and set initial supply to 0. 24 | - Update the contract address in the [addresses.ts](./constants/addresses.ts) file. 25 | - Add your wallet's private key as an environment variable in a `.env.local` file called `PRIVATE_KEY`: 26 | 27 | ```text title=".env.local" 28 | PRIVATE_KEY=your-wallet-private-key 29 | ``` 30 | 31 | - Create a Magic Auth app on Magic.link and add your publishable key to the `.env.local` file: 32 | 33 | ```text title=".env.local" 34 | NEXT_PUBLIC_MAGIC_LINK_API_KEY=your-magic-publishable-key 35 | ``` 36 | 37 | - Create a stripe account and add your publishable key and secret key to the `.env.local` file: 38 | 39 | ```text title=".env.local" 40 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your-stripe-publishable-key 41 | STRIPE_SECRET_KEY=your-stripe-secret-key 42 | ``` 43 | 44 | - We are going to setup a webhook which will be used to securely determine that the transaction was successful. To setup the webhook on the development environment, we will use the [stripe cli](https://stripe.com/docs/stripe-cli). 45 | - Install the [stripe cli](https://stripe.com/docs/stripe-cli) and run `stripe login` to login to your stripe account. 46 | - Forward the webhook to your local server by running `stripe listen --forward-to localhost:3000/api/webhook` in a separate terminal. 47 | - Paste the webhook secret key in the `.env.local` file: 48 | 49 | ```text title=".env.local" 50 | WEBHOOK_SECRET_KEY=your-stripe-webhook-secret 51 | ``` 52 | 53 | - Install dependencies: 54 | 55 | ```bash 56 | yarn # yarn 57 | 58 | npm install # npm 59 | ``` 60 | 61 | - Run the development server: 62 | 63 | ```bash 64 | yarn dev # yarn 65 | 66 | npm run dev # npm 67 | ``` 68 | 69 | ## Join our Discord! 70 | 71 | For any questions, suggestions, join our discord at [https://discord.gg/thirdweb](https://discord.gg/thirdweb). 72 | -------------------------------------------------------------------------------- /components/Form.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | PaymentElement, 3 | useElements, 4 | useStripe, 5 | } from "@stripe/react-stripe-js"; 6 | import styles from "../styles/Home.module.css"; 7 | import React, { useEffect, useState } from "react"; 8 | import { EDITION_ADDRESS } from "../constants/addresses"; 9 | 10 | const Form = () => { 11 | const elements = useElements(); 12 | const stripe = useStripe(); 13 | const [isLoading, setIsLoading] = useState(false); 14 | const [message, setMessage] = useState(null); 15 | const [isSuccess, setIsSuccess] = useState(false); 16 | 17 | useEffect(() => { 18 | if (!stripe) { 19 | return; 20 | } 21 | 22 | const clientSecret = new URLSearchParams(window.location.search).get( 23 | "payment_intent_client_secret" 24 | ); 25 | 26 | if (!clientSecret) { 27 | return; 28 | } 29 | 30 | stripe.retrievePaymentIntent(clientSecret).then(({ paymentIntent }) => { 31 | switch (paymentIntent?.status) { 32 | case "succeeded": 33 | setIsSuccess(true); 34 | setMessage("Your payment was successfull!"); 35 | break; 36 | case "processing": 37 | setMessage("Your payment is processing."); 38 | break; 39 | case "requires_payment_method": 40 | setMessage("Your payment was not successful, please try again."); 41 | break; 42 | default: 43 | setMessage("Something went wrong."); 44 | break; 45 | } 46 | }); 47 | }, [stripe]); 48 | 49 | const URL = process.env.NEXT_PUBLIC_DOMAIN || "http://localhost:3000"; 50 | 51 | const handleSubmit = async (e: React.FormEvent) => { 52 | e.preventDefault(); 53 | 54 | if (!stripe || !elements) { 55 | return console.log("not loaded"); 56 | } 57 | 58 | setIsLoading(true); 59 | 60 | const { error } = await stripe.confirmPayment({ 61 | elements, 62 | confirmParams: { 63 | return_url: URL, 64 | }, 65 | }); 66 | 67 | if (error.type === "card_error" || error.type === "validation_error") { 68 | setMessage(error.message); 69 | } else { 70 | setMessage("An unexpected error occured."); 71 | } 72 | 73 | setIsLoading(false); 74 | }; 75 | 76 | return ( 77 | <> 78 | {message ? ( 79 | <> 80 | {isSuccess && ( 81 | 87 | Check out your NFT 88 | 89 | )} 90 |

{message}

91 | 92 | ) : ( 93 |
94 | 95 | 101 | 102 | )} 103 | 104 | ); 105 | }; 106 | 107 | export default Form; 108 | -------------------------------------------------------------------------------- /constants/addresses.ts: -------------------------------------------------------------------------------- 1 | export const EDITION_ADDRESS = "0xAfE054Bd560225C543D169F58F74C331A36117E7"; 2 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | basePath: "", 5 | }; 6 | 7 | module.exports = nextConfig; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seamless-nft-exp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "export": "next export", 11 | "deploy": "next build && next export && npx thirdweb@latest upload out" 12 | }, 13 | "dependencies": { 14 | "@stripe/react-stripe-js": "^1.16.3", 15 | "@stripe/stripe-js": "^1.46.0", 16 | "@thirdweb-dev/react": "^3", 17 | "@thirdweb-dev/sdk": "^3", 18 | "ethers": "^5.7.2", 19 | "micro": "^10.0.1", 20 | "next": "^13", 21 | "react": "^18.2", 22 | "react-dom": "^18.2", 23 | "stripe": "^11.6.0" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^18.11.11", 27 | "@types/react": "^18", 28 | "typescript": "^4.9.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ChainId, ThirdwebProvider } from "@thirdweb-dev/react"; 2 | import { MagicConnector } from "@thirdweb-dev/react/evm/connectors/magic"; 3 | import type { AppProps } from "next/app"; 4 | import "../styles/globals.css"; 5 | 6 | // This is the chainId your dApp will work on. 7 | const activeChainId = ChainId.Mumbai; 8 | 9 | const magicLinkConnector = new MagicConnector({ 10 | options: { 11 | apiKey: process.env.NEXT_PUBLIC_MAGIC_LINK_API_KEY as string, 12 | rpcUrls: { 13 | [ChainId.Mumbai]: "https://rpc-mumbai.maticvigil.com", 14 | }, 15 | }, 16 | }); 17 | 18 | // Array of wallet connectors you want to use for your dApp. 19 | const connectors = [magicLinkConnector]; 20 | 21 | function MyApp({ Component, pageProps }: AppProps) { 22 | return ( 23 | 27 | 28 | 29 | ); 30 | } 31 | 32 | export default MyApp; 33 | -------------------------------------------------------------------------------- /pages/api/stripe_intent.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import Stripe from "stripe"; 3 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { 4 | apiVersion: "2022-11-15", 5 | }); 6 | 7 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 8 | const { address } = req.body; 9 | 10 | const amount = 10000; 11 | 12 | try { 13 | const payment_intent = await stripe.paymentIntents.create({ 14 | amount: amount, 15 | currency: "usd", 16 | description: "Payment description", 17 | automatic_payment_methods: { 18 | enabled: true, 19 | }, 20 | metadata: { address }, 21 | }); 22 | 23 | return res.status(200).json(payment_intent); 24 | } catch (err) { 25 | const errorMessage = 26 | err instanceof Error ? err.message : "Internal server error"; 27 | return res.status(500).json({ statusCode: 500, message: errorMessage }); 28 | } 29 | }; 30 | 31 | export default handler; 32 | -------------------------------------------------------------------------------- /pages/api/webhook.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | import { buffer } from "micro"; 3 | import { NextApiRequest, NextApiResponse } from "next"; 4 | import { ThirdwebSDK } from "@thirdweb-dev/sdk"; 5 | import { EDITION_ADDRESS } from "../../constants/addresses"; 6 | 7 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, { 8 | apiVersion: "2022-11-15", 9 | }); 10 | 11 | const webhookSecret = process.env.WEBHOOK_SECRET_KEY as string; 12 | 13 | export const config = { 14 | api: { 15 | bodyParser: false, 16 | }, 17 | }; 18 | 19 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 20 | const buf = await buffer(req); 21 | const sig = req.headers["stripe-signature"]; 22 | 23 | const sdk = ThirdwebSDK.fromPrivateKey( 24 | process.env.PRIVATE_KEY as string, 25 | "mumbai" 26 | ); 27 | 28 | const nftCollection = await sdk.getContract(EDITION_ADDRESS, "edition"); 29 | 30 | let event; 31 | 32 | if (buf && sig) { 33 | try { 34 | event = stripe.webhooks.constructEvent(buf, sig, webhookSecret); 35 | } catch (err) { 36 | return res.status(400).send(`Webhook Error: ${(err as Error).message}`); 37 | } 38 | 39 | const data = JSON.parse(String(buf)); 40 | 41 | if (event.type === "payment_intent.succeeded") { 42 | const paymentMethod = event.data.object as any; 43 | const address = paymentMethod.metadata.address; 44 | 45 | const tx = await nftCollection.erc1155.mintAdditionalSupplyTo( 46 | address, 47 | 0, 48 | 1 49 | ); 50 | 51 | console.log(tx); 52 | 53 | console.log( 54 | `PaymentIntent was successfull for: ${data.data.object.amount}` 55 | ); 56 | } 57 | } 58 | return res.json({ received: true }); 59 | }; 60 | 61 | export default handler; 62 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Elements } from "@stripe/react-stripe-js"; 2 | import { 3 | Appearance, 4 | loadStripe, 5 | StripeElementsOptions, 6 | } from "@stripe/stripe-js"; 7 | import { 8 | ThirdwebNftMedia, 9 | useAddress, 10 | useContract, 11 | useNFT, 12 | } from "@thirdweb-dev/react"; 13 | import { useMagic } from "@thirdweb-dev/react/evm/connectors/magic"; 14 | import type { NextPage } from "next"; 15 | import { useEffect, useState } from "react"; 16 | import Form from "../components/Form"; 17 | import { EDITION_ADDRESS } from "../constants/addresses"; 18 | import styles from "../styles/Home.module.css"; 19 | 20 | const Home: NextPage = () => { 21 | const address = useAddress(); 22 | const connectWithMagic = useMagic(); 23 | const [email, setEmail] = useState(""); 24 | const { contract } = useContract(EDITION_ADDRESS, "edition"); 25 | const { data: nft } = useNFT(contract, 0); 26 | const [clientSecret, setClientSecret] = useState(""); 27 | 28 | const stripe = loadStripe( 29 | process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string 30 | ); 31 | 32 | const appearance: Appearance = { 33 | theme: "night", 34 | labels: "above", 35 | }; 36 | 37 | const options: StripeElementsOptions = { 38 | clientSecret, 39 | appearance, 40 | }; 41 | 42 | useEffect(() => { 43 | if (address) { 44 | fetch("/api/stripe_intent", { 45 | method: "POST", 46 | headers: { "Content-Type": "application/json" }, 47 | body: JSON.stringify({ 48 | address, 49 | }), 50 | }) 51 | .then((res) => res.json()) 52 | .then((data) => { 53 | setClientSecret(data.client_secret); 54 | }); 55 | } 56 | }, [address]); 57 | 58 | return ( 59 |
60 | {address ? ( 61 | <> 62 |

You are signed in as: {address}

63 |
64 | {nft?.metadata && ( 65 | 69 | )} 70 |

{nft?.metadata?.name}

71 |

{nft?.metadata?.description}

72 |

Price: 100$

73 |
74 | {clientSecret && ( 75 | 76 |
77 | 78 | )} 79 | 80 | ) : ( 81 | <> 82 |

Login With Email

83 | { 85 | e.preventDefault(); 86 | connectWithMagic({ email }); 87 | }} 88 | style={{ 89 | width: 500, 90 | maxWidth: "90vw", 91 | display: "flex", 92 | alignItems: "center", 93 | justifyContent: "center", 94 | flexDirection: "row", 95 | gap: 16, 96 | }} 97 | > 98 | setEmail(e.target.value)} 104 | /> 105 | 106 |
107 | 108 | )} 109 |
110 | ); 111 | }; 112 | 113 | export default Home; 114 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/seamless-stripe-checkout/12b7806176a7ea1c6ec723cd321a72881d1450c7/public/favicon.ico -------------------------------------------------------------------------------- /public/thirdweb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Seamless NFT Experience 2 | 3 | This project demonstrates how you can create a seamless NFT experience for your users. 4 | 5 | We use Magic.link to authenticate users using their email, then we use stripe to collect payment for the NFTs and send them to the user's wallet generated by Magic. 6 | 7 | ## Using This Template 8 | 9 | Create a project using this example: 10 | 11 | ```bash 12 | npx thirdweb create --template seamless-stripe-checkout 13 | ``` 14 | 15 | - Create an [Edition](https://thirdweb.com/thirdweb.eth/TokenERC1155) contract using the dashboard. 16 | - Add an NFT to the token contract with your desired metadata and set initial supply to 0. 17 | - Update the contract address in the [addresses.ts](./constants/addresses.ts) file. 18 | - Add your wallet's private key as an environment variable in a `.env.local` file called `PRIVATE_KEY`: 19 | 20 | ```text title=".env.local" 21 | PRIVATE_KEY=your-wallet-private-key 22 | ``` 23 | 24 | - Create a Magic Auth app on Magic.link and add your publishable key to the `.env.local` file: 25 | 26 | ```text title=".env.local" 27 | NEXT_PUBLIC_MAGIC_LINK_API_KEY=your-magic-publishable-key 28 | ``` 29 | 30 | - Create a stripe account and add your publishable key and secret key to the `.env.local` file: 31 | 32 | ```text title=".env.local" 33 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your-stripe-publishable-key 34 | STRIPE_SECRET_KEY=your-stripe-secret-key 35 | ``` 36 | 37 | - We are going to setup a webhook which will be used to securely determine that the transaction was successful. To setup the webhook on the development environment, we will use the [stripe cli](https://stripe.com/docs/stripe-cli). 38 | - Install the [stripe cli](https://stripe.com/docs/stripe-cli) and run `stripe login` to login to your stripe account. 39 | - Forward the webhook to your local server by running `stripe listen --forward-to localhost:3000/api/webhook` in a separate terminal. 40 | - Paste the webhook secret key in the `.env.local` file: 41 | 42 | ```text title=".env.local" 43 | WEBHOOK_SECRET_KEY=your-stripe-webhook-secret 44 | ``` 45 | 46 | - Install dependencies: 47 | 48 | ```bash 49 | yarn # yarn 50 | 51 | npm install # npm 52 | ``` 53 | 54 | - Run the development server: 55 | 56 | ```bash 57 | yarn dev # yarn 58 | 59 | npm run dev # npm 60 | ``` 61 | 62 | ## Join our Discord! 63 | 64 | For any questions, suggestions, join our discord at [https://discord.gg/thirdweb](https://discord.gg/thirdweb). 65 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-top: 96px; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: center; 7 | width: 100vw; 8 | padding: 0 24px; 9 | } 10 | 11 | .h1 { 12 | margin-bottom: 0px; 13 | } 14 | 15 | .explain { 16 | font-size: 1.125rem; 17 | } 18 | 19 | .purple { 20 | color: #9f2c9d; 21 | } 22 | 23 | .divider { 24 | width: 50%; 25 | border-color: grey; 26 | opacity: 0.25; 27 | } 28 | 29 | .mainButton { 30 | cursor: pointer; 31 | display: inline-flex; 32 | appearance: none; 33 | align-items: center; 34 | -webkit-box-align: center; 35 | justify-content: center; 36 | vertical-align: middle; 37 | line-height: 1.2; 38 | font-weight: 600; 39 | transition-property: background-color, border-color, color, fill, stroke, 40 | opacity, box-shadow, transform; 41 | transition-duration: 200ms; 42 | height: 3rem; 43 | min-width: 3rem; 44 | font-size: 1rem; 45 | background: #e5e5ea; 46 | background-image: linear-gradient(to left, #cc25b3 0%, #418dff 101.52%); 47 | color: #fff; 48 | width: 180px; 49 | text-align: center; 50 | border-radius: 9999px; 51 | outline: none; 52 | border: none; 53 | } 54 | 55 | .mainButton:hover { 56 | opacity: 0.8; 57 | } 58 | 59 | .textInput { 60 | width: 75%; 61 | background-color: transparent; 62 | border: 1px solid grey; 63 | border-radius: 8px; 64 | color: #fff; 65 | height: 48px; 66 | padding: 0 16px; 67 | font-size: 1rem; 68 | margin-bottom: 16px; 69 | } 70 | 71 | .textInput:focus { 72 | outline: none; 73 | } 74 | 75 | .payButton { 76 | margin: 20px auto; 77 | align-self: center; 78 | } 79 | 80 | .PaymentForm { 81 | display: flex; 82 | align-items: center; 83 | justify-content: center; 84 | flex-direction: column; 85 | } 86 | 87 | .nftCard { 88 | display: flex; 89 | flex-direction: column; 90 | align-items: center; 91 | justify-content: center; 92 | } 93 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: #7dd3fc; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | html { 19 | color-scheme: dark; 20 | } 21 | body { 22 | color: #e7e8e8; 23 | background: #0f1318; 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | --------------------------------------------------------------------------------