Skip to content

Commit

Permalink
DX:2038: App with Stripe paywall example (#150)
Browse files Browse the repository at this point in the history
  • Loading branch information
mettin authored Jul 17, 2023
1 parent 934570a commit 7607d12
Show file tree
Hide file tree
Showing 23 changed files with 1,623 additions and 795 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,13 @@ npx create-miro-app@latest

### Full-stack apps

| | Description |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [github-appcards](examples/github-appcards) | This full-stack example shows how to build an integration with GitHub that syncs data between GitHub issues and Miro app cards. |
| [plant-uml](https://github.com/miroapp/miro-plantuml) | This full-stack example shows how to import [Plant UML](https://plantuml.com/) diagrams into Miro as editable board items. |
| [nextjs](examples/nextjs-full-stack) | This full-stack example shows a Next.js application that uploads a camera image to the Miro board using Web SDK and REST API integration. |
| [webhooks-manager](examples/webhooks-manager/) | This full-stack example demonstrates how to interact with the webhooks API, and how to handle the webhooks challenge. |
| | Description |
| ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [monetization-with-stripe](examples/monetization-with-stripe) | This full-stack example shows how use Stripe to add a paywall for certain features in your app. |
| [github-appcards](examples/github-appcards) | This full-stack example shows how to build an integration with GitHub that syncs data between GitHub issues and Miro app cards. |
| [plant-uml](https://github.com/miroapp/miro-plantuml) | This full-stack example shows how to import [Plant UML](https://plantuml.com/) diagrams into Miro as editable board items. |
| [nextjs](examples/nextjs-full-stack) | This full-stack example shows a Next.js application that uploads a camera image to the Miro board using Web SDK and REST API integration. |
| [webhooks-manager](examples/webhooks-manager/) | This full-stack example demonstrates how to interact with the webhooks API, and how to handle the webhooks challenge. |

<p>&nbsp;</p>

Expand Down
8 changes: 8 additions & 0 deletions examples/monetization-with-stripe/.env.example
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=
25 changes: 25 additions & 0 deletions examples/monetization-with-stripe/.gitignore
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
49 changes: 49 additions & 0 deletions examples/monetization-with-stripe/README.md
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).
8 changes: 8 additions & 0 deletions examples/monetization-with-stripe/app-manifest.yaml
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
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 examples/monetization-with-stripe/components/PaidFeature.tsx
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 examples/monetization-with-stripe/components/PaywallNotice.tsx
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>
)}
</>
);
};
2 changes: 2 additions & 0 deletions examples/monetization-with-stripe/global.d.ts
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" />
47 changes: 47 additions & 0 deletions examples/monetization-with-stripe/initMiro.ts
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,
};
}
5 changes: 5 additions & 0 deletions examples/monetization-with-stripe/next-env.d.ts
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.
31 changes: 31 additions & 0 deletions examples/monetization-with-stripe/package.json
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"
}
}
6 changes: 6 additions & 0 deletions examples/monetization-with-stripe/pages/_app.tsx
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} />;
}
20 changes: 20 additions & 0 deletions examples/monetization-with-stripe/pages/_document.tsx
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 examples/monetization-with-stripe/pages/api/payment-handler.ts
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;
Loading

2 comments on commit 7607d12

@vercel
Copy link

@vercel vercel bot commented on 7607d12 Jul 17, 2023

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

@vercel
Copy link

@vercel vercel bot commented on 7607d12 Jul 17, 2023

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

Please sign in to comment.