-
Notifications
You must be signed in to change notification settings - Fork 210
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DX:2038: App with Stripe paywall example (#150)
- Loading branch information
Showing
23 changed files
with
1,623 additions
and
795 deletions.
There are no files selected for viewing
This file contains 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 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,8 @@ | ||
MIRO_CLIENT_ID="" | ||
MIRO_CLIENT_SECRET="" | ||
MIRO_REDIRECT_URL="http://localhost:3000/api/redirect/" | ||
|
||
# https://dashboard.stripe.com/apikeys | ||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= | ||
STRIPE_SECRET_KEY= | ||
STRIPE_WEBHOOK_ENDPOINT_SECRET= |
This file contains 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,25 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
.next | ||
|
||
# testing | ||
/coverage | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
.idea | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
|
||
# local env files | ||
.env | ||
dist | ||
/store.json |
This file contains 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,49 @@ | ||
# Setup | ||
|
||
This app has a paywall for a payed feature. To enable the payed feature, users need to pay a one time payment using Stripe. | ||
|
||
## On Miro | ||
|
||
1. [Sign in](https://miro.com/login/) to Miro, and then create a [Developer team](https://developers.miro.com/docs/create-a-developer-team) under your user account, if you haven't yet. | ||
1. [Click here to create your app in Miro](https://miro.com/app/settings/user-profile/apps/?appTemplate=%7B%22appName%22%3A%22App+With+Payments%22%2C%22sdkUri%22%3A%22http%3A%2F%2Flocalhost%3A3000%22%2C%22redirectUris%22%3A%5B%22http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fredirect%2F%22%5D%2C%22scopes%22%3A%5B%22boards%3Aread%22%2C%22boards%3Awrite%22%5D%7D). | ||
1. Configure the app: | ||
|
||
- Go to **Redirect URI for OAuth2.0**, click **Options**. for the localhost path. \ | ||
From the drop-down menu select **Use this URI for SDK authorization**. | ||
|
||
1. Rename the ['.env.example' file](.env.example) to `.env` | ||
1. Open the [.env file](.env), and enter the app client ID and client secret that you can find on top of the app settings page | ||
|
||
## On Stripe | ||
|
||
1. [Create a account](https://dashboard.stripe.com/register) or [login](https://dashboard.stripe.com/login) on Stripe | ||
2. Create a [payment link](https://dashboard.stripe.com/test/payment-links) | ||
3. Update the link in the [PaywallNotice component](./components/PaywallNotice.tsx) | ||
4. Add your [API keys](https://dashboard.stripe.com/apikeys) to the [.env file](.env) | ||
5. For development, in your terminal: | ||
1. `stripe login` | ||
2. `stripe listen --forward-to localhost:3000/api/payment-handler` | ||
3. Copy the webhook signing secret from console output | ||
|
||
### Local development setup | ||
|
||
_For development only, do **not** use this storage solution in production_: | ||
|
||
``` | ||
touch store.json && echo '{}' > store.json | ||
``` | ||
|
||
Run `npm start` to start developing. | ||
|
||
### When your server is up and running: | ||
|
||
- Go to [the dashboard on Miro.com](https://miro.com/app/dashboard). | ||
- In the left-hand team selector, select your developer team and open any board, or create a new one. | ||
- To start your app, click the app icon in the app toolbar on the left. | ||
|
||
#### Stripe test card | ||
|
||
**CC**: `4242 4242 4242 4242`\ | ||
**Date**: Any valid date\ | ||
**CVC**: Any 3 digits\ | ||
More details on [the stripe docs](https://stripe.com/docs/testing). |
This file contains 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,8 @@ | ||
# See https://developers.miro.com/docs/app-manifest on how to use this | ||
appName: Monetization with Stripe | ||
sdkUri: "http://localhost:3000" | ||
redirectUris: | ||
- http://localhost:3000/api/redirect/ | ||
scopes: | ||
- boards:read | ||
- boards:write |
23 changes: 23 additions & 0 deletions
23
examples/monetization-with-stripe/components/GenerallyAvailableFeature.tsx
This file contains 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,23 @@ | ||
export const GenerallyAvailableFeature = () => { | ||
const handleClick = () => { | ||
const placeSticky = async () => { | ||
await window.miro?.board?.createStickyNote({ | ||
content: `This is sticky note. \n Added to the board at ${new Date()}`, | ||
}); | ||
}; | ||
|
||
placeSticky(); | ||
}; | ||
|
||
return ( | ||
<div> | ||
<button | ||
className={"button button-primary"} | ||
type={"button"} | ||
onClick={handleClick} | ||
> | ||
Add a plain sticky | ||
</button> | ||
</div> | ||
); | ||
}; |
27 changes: 27 additions & 0 deletions
27
examples/monetization-with-stripe/components/PaidFeature.tsx
This file contains 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,27 @@ | ||
export const PaidFeature = () => { | ||
const handleClick = () => { | ||
const placeSticky = async () => { | ||
await window.miro?.board?.createStickyNote({ | ||
content: `This is a sticky note. It looks just like the actual paper one.\n Added to the board at ${new Date()}`, | ||
style: { | ||
textAlign: "left", | ||
fillColor: "cyan", | ||
}, | ||
}); | ||
}; | ||
|
||
placeSticky(); | ||
}; | ||
|
||
return ( | ||
<div> | ||
<button | ||
className={"button button-primary"} | ||
type={"button"} | ||
onClick={handleClick} | ||
> | ||
Add styled sticky | ||
</button> | ||
</div> | ||
); | ||
}; |
43 changes: 43 additions & 0 deletions
43
examples/monetization-with-stripe/components/PaywallNotice.tsx
This file contains 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,43 @@ | ||
import { FC, useState } from "react"; | ||
|
||
export const PaywallNotice: FC<{ userId: string }> = ({ userId }) => { | ||
const [hasClickedBuy, setHasClickedBuy] = useState(false); | ||
|
||
const handleClickBuy = () => { | ||
setHasClickedBuy(true); | ||
}; | ||
|
||
const paymentLink = "https://buy.stripe.com/test_abcdefghijkl21347890"; | ||
|
||
return ( | ||
<> | ||
<h2>🧱🧱🧱🧱 Paywall 🧱🧱🧱🧱</h2> | ||
|
||
<p> | ||
This app has advanced features that are only available for paying users. | ||
</p> | ||
<a | ||
href={`${paymentLink}?client_reference_id=${userId}&utm_source=miro-app-panel`} | ||
target="_blank" | ||
className={"button button-primary"} | ||
onClick={handleClickBuy} | ||
> | ||
Buy ($5 once) | ||
</a> | ||
|
||
{hasClickedBuy && ( | ||
<div> | ||
<hr /> | ||
<p>Done? Click the button below to validate your payment.</p> | ||
<button | ||
type="button" | ||
className="button button-primary" | ||
onClick={() => window.location.reload()} | ||
> | ||
I've paid! | ||
</button> | ||
</div> | ||
)} | ||
</> | ||
); | ||
}; |
This file contains 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,2 @@ | ||
// https://vitejs.dev/guide/features.html#typescript-compiler-options | ||
/// <reference types="vite/client" /> |
This file contains 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,47 @@ | ||
import { Miro } from "@mirohq/miro-api"; | ||
import { serialize } from "cookie"; | ||
|
||
function getSerializedCookie(name: string, value: string | number) { | ||
return serialize(name, String(value), { | ||
path: "/", | ||
httpOnly: true, | ||
sameSite: "none", | ||
secure: true, | ||
}); | ||
} | ||
|
||
export default function initMiro( | ||
request: { cookies: Record<string, undefined | string> }, | ||
response?: { | ||
setHeader(name: string, value: string[]): void; | ||
} | ||
) { | ||
const tokensCookie = "miro_tokens"; | ||
const userIdCookie = "miro_user_id"; | ||
|
||
// setup a Miro instance that loads tokens from cookies | ||
return { | ||
miro: new Miro({ | ||
storage: { | ||
get: () => { | ||
// Load state (tokens) from a cookie if it's set | ||
try { | ||
return JSON.parse(request.cookies[tokensCookie] || "null"); | ||
} catch (err) { | ||
return null; | ||
} | ||
}, | ||
set: (userId, state) => { | ||
// store state (tokens) in the cookie | ||
response && | ||
"setHeader" in response && | ||
response.setHeader("Set-Cookie", [ | ||
getSerializedCookie(tokensCookie, JSON.stringify(state)), | ||
getSerializedCookie(userIdCookie, userId), | ||
]); | ||
}, | ||
}, | ||
}), | ||
userId: JSON.parse(request.cookies[tokensCookie] || "null")?.userId, | ||
}; | ||
} |
This file contains 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,5 @@ | ||
/// <reference types="next" /> | ||
/// <reference types="next/image-types/global" /> | ||
|
||
// NOTE: This file should not be edited | ||
// see https://nextjs.org/docs/basic-features/typescript for more information. |
This file contains 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,31 @@ | ||
{ | ||
"name": "monetization-with-stripe", | ||
"description": "Add a paywall to your app by adding a Stripe integration", | ||
"version": "0.1.0", | ||
"keywords": ["Next.js", "Miro SDK", "Stripe", "TypeScript"], | ||
"license": "MIT", | ||
"scripts": { | ||
"build": "next build", | ||
"start": "next dev", | ||
"lint": "next lint" | ||
}, | ||
"dependencies": { | ||
"@mirohq/miro-api": "^1.0.2", | ||
"@stripe/react-stripe-js": "^2.1.0", | ||
"cookie": "^0.5.0", | ||
"cookies": "^0.8.0", | ||
"dotenv": "^16.0.3", | ||
"micro": "^10.0.1", | ||
"next": "^13.4.10", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0", | ||
"stripe": "^12.3.0" | ||
}, | ||
"devDependencies": { | ||
"@mirohq/websdk-types": "latest", | ||
"@types/cookie": "^0.5.1", | ||
"@types/node": "^18.8.2", | ||
"@types/react": "^18.0.24", | ||
"typescript": "4.8.4" | ||
} | ||
} |
This file contains 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,6 @@ | ||
import "../styles/globals.css"; | ||
import type { AppProps } from "next/app"; | ||
|
||
export default function MyApp({ Component, pageProps }: AppProps) { | ||
return <Component {...pageProps} />; | ||
} |
This file contains 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,20 @@ | ||
import { Html, Head, Main, NextScript } from "next/document"; | ||
|
||
export default function Document() { | ||
return ( | ||
<Html> | ||
<Head> | ||
<link | ||
rel="stylesheet" | ||
href="https://unpkg.com/mirotone/dist/styles.css" | ||
></link> | ||
<script src="https://miro.com/app/static/sdk/v2/miro.js" /> | ||
<title>Monetization with Stripe</title> | ||
</Head> | ||
<body> | ||
<Main /> | ||
<NextScript /> | ||
</body> | ||
</Html> | ||
); | ||
} |
61 changes: 61 additions & 0 deletions
61
examples/monetization-with-stripe/pages/api/payment-handler.ts
This file contains 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,61 @@ | ||
import { buffer } from "micro"; | ||
import { storage } from "../../utils/storage"; | ||
import { NextApiRequest, NextApiResponse } from "next"; | ||
|
||
import Stripe from "stripe"; | ||
import { ExternalUserId } from "@mirohq/miro-api"; | ||
|
||
export const PAYMENT_STORAGE_KEY = "paid-for-[miro-integration-name]"; | ||
|
||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, { | ||
apiVersion: "2022-11-15", | ||
}); | ||
const webhookSecret = process.env.STRIPE_WEBHOOK_ENDPOINT_SECRET as string; | ||
|
||
export const config = { | ||
api: { | ||
bodyParser: false, | ||
}, | ||
}; | ||
|
||
const handler = async (req: NextApiRequest, res: NextApiResponse) => { | ||
if (req.method === "POST") { | ||
const buf = await buffer(req); | ||
const sig = req.headers["stripe-signature"]; | ||
|
||
let stripeEvent; | ||
try { | ||
stripeEvent = stripe.webhooks.constructEvent( | ||
buf, | ||
sig || "", | ||
webhookSecret | ||
); | ||
} catch (err) { | ||
res | ||
.status(400) | ||
.send(`Webhook Error: ${err instanceof Error ? err.message : err}`); | ||
return; | ||
} | ||
|
||
if ("checkout.session.completed" === stripeEvent.type) { | ||
const session = stripeEvent.data.object; | ||
|
||
/* 🚨 IMPORTANT 🚨 */ | ||
// You probably want to store this in your backend | ||
// made a simple file based storage for demo purposes | ||
await storage.set( | ||
(session as { client_reference_id: ExternalUserId }) | ||
.client_reference_id, | ||
PAYMENT_STORAGE_KEY, | ||
"true" | ||
); | ||
} | ||
|
||
res.json({ received: true }); | ||
} else { | ||
res.setHeader("Allow", "POST"); | ||
res.status(405).end("Method Not Allowed"); | ||
} | ||
}; | ||
|
||
export default handler; |
Oops, something went wrong.
7607d12
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
webhooks-manager – ./examples/webhooks-manager
webhooks-manager-git-main-miro-web.vercel.app
webhooks-manager-miro-web.vercel.app
webhooks-manager-sepia.vercel.app
7607d12
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
app-examples-wordle – ./examples/wordle
app-examples-wordle-anthonyroux.vercel.app
app-examples-wordle.vercel.app
app-examples-wordle-git-main-anthonyroux.vercel.app