diff --git a/README.md b/README.md index d961a12d5..e5ce1a6a3 100644 --- a/README.md +++ b/README.md @@ -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. |

 

diff --git a/examples/monetization-with-stripe/.env.example b/examples/monetization-with-stripe/.env.example new file mode 100644 index 000000000..2479fa161 --- /dev/null +++ b/examples/monetization-with-stripe/.env.example @@ -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= diff --git a/examples/monetization-with-stripe/.gitignore b/examples/monetization-with-stripe/.gitignore new file mode 100644 index 000000000..5428f30ae --- /dev/null +++ b/examples/monetization-with-stripe/.gitignore @@ -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 diff --git a/examples/monetization-with-stripe/README.md b/examples/monetization-with-stripe/README.md new file mode 100644 index 000000000..ec8496017 --- /dev/null +++ b/examples/monetization-with-stripe/README.md @@ -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). diff --git a/examples/monetization-with-stripe/app-manifest.yaml b/examples/monetization-with-stripe/app-manifest.yaml new file mode 100644 index 000000000..bc6a80336 --- /dev/null +++ b/examples/monetization-with-stripe/app-manifest.yaml @@ -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 diff --git a/examples/monetization-with-stripe/components/GenerallyAvailableFeature.tsx b/examples/monetization-with-stripe/components/GenerallyAvailableFeature.tsx new file mode 100644 index 000000000..798ef9368 --- /dev/null +++ b/examples/monetization-with-stripe/components/GenerallyAvailableFeature.tsx @@ -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 ( +
+ +
+ ); +}; diff --git a/examples/monetization-with-stripe/components/PaidFeature.tsx b/examples/monetization-with-stripe/components/PaidFeature.tsx new file mode 100644 index 000000000..40c37cafd --- /dev/null +++ b/examples/monetization-with-stripe/components/PaidFeature.tsx @@ -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 ( +
+ +
+ ); +}; diff --git a/examples/monetization-with-stripe/components/PaywallNotice.tsx b/examples/monetization-with-stripe/components/PaywallNotice.tsx new file mode 100644 index 000000000..f3e18142b --- /dev/null +++ b/examples/monetization-with-stripe/components/PaywallNotice.tsx @@ -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 ( + <> +

🧱🧱🧱🧱 Paywall 🧱🧱🧱🧱

+ +

+ This app has advanced features that are only available for paying users. +

+ + Buy ($5 once) + + + {hasClickedBuy && ( +
+
+

Done? Click the button below to validate your payment.

+ +
+ )} + + ); +}; diff --git a/examples/monetization-with-stripe/global.d.ts b/examples/monetization-with-stripe/global.d.ts new file mode 100644 index 000000000..18b40cc0f --- /dev/null +++ b/examples/monetization-with-stripe/global.d.ts @@ -0,0 +1,2 @@ +// https://vitejs.dev/guide/features.html#typescript-compiler-options +/// diff --git a/examples/monetization-with-stripe/initMiro.ts b/examples/monetization-with-stripe/initMiro.ts new file mode 100644 index 000000000..011ff64bd --- /dev/null +++ b/examples/monetization-with-stripe/initMiro.ts @@ -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 }, + 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, + }; +} diff --git a/examples/monetization-with-stripe/next-env.d.ts b/examples/monetization-with-stripe/next-env.d.ts new file mode 100644 index 000000000..4f11a03dc --- /dev/null +++ b/examples/monetization-with-stripe/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/monetization-with-stripe/package.json b/examples/monetization-with-stripe/package.json new file mode 100644 index 000000000..2e80767f8 --- /dev/null +++ b/examples/monetization-with-stripe/package.json @@ -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" + } +} diff --git a/examples/monetization-with-stripe/pages/_app.tsx b/examples/monetization-with-stripe/pages/_app.tsx new file mode 100644 index 000000000..42e2aed31 --- /dev/null +++ b/examples/monetization-with-stripe/pages/_app.tsx @@ -0,0 +1,6 @@ +import "../styles/globals.css"; +import type { AppProps } from "next/app"; + +export default function MyApp({ Component, pageProps }: AppProps) { + return ; +} diff --git a/examples/monetization-with-stripe/pages/_document.tsx b/examples/monetization-with-stripe/pages/_document.tsx new file mode 100644 index 000000000..5d29dbfce --- /dev/null +++ b/examples/monetization-with-stripe/pages/_document.tsx @@ -0,0 +1,20 @@ +import { Html, Head, Main, NextScript } from "next/document"; + +export default function Document() { + return ( + + + +