From 791a20322b955a2558b6b8688c67a8206e5bece7 Mon Sep 17 00:00:00 2001 From: David Cheung Date: Mon, 26 Apr 2021 19:09:42 -0400 Subject: [PATCH 1/3] stripe initial impl --- templates/.github/workflows/ci.yml | 6 +- templates/package.json | 6 +- templates/src/App.js | 14 +++- templates/src/components/Navigation.js | 10 ++- templates/src/config/development.json | 3 +- templates/src/config/production.json | 3 +- templates/src/config/staging.json | 3 +- templates/src/pages/Billing/Confirmation.js | 17 ++++ templates/src/pages/Billing/Plans.css | 47 +++++++++++ templates/src/pages/Billing/Plans.js | 91 +++++++++++++++++++++ templates/src/pages/Billing/routes.js | 23 ++++++ zero-module.yml | 30 ++++++- 12 files changed, 239 insertions(+), 14 deletions(-) create mode 100644 templates/src/pages/Billing/Confirmation.js create mode 100644 templates/src/pages/Billing/Plans.css create mode 100644 templates/src/pages/Billing/Plans.js create mode 100644 templates/src/pages/Billing/routes.js diff --git a/templates/.github/workflows/ci.yml b/templates/.github/workflows/ci.yml index 800deb1..d699d9e 100644 --- a/templates/.github/workflows/ci.yml +++ b/templates/.github/workflows/ci.yml @@ -18,9 +18,9 @@ jobs: yarn yarn test --watchAll=false - if: env.AWS_ACCESS_KEY_ID == null - run: | - echo "AWS Credentials not found, This is expected for the first run as the repo is provisioned then secrets are injected at a later step." - exit 1 + run: | + echo "AWS Credentials not found, This is expected for the first run as the repo is provisioned then secrets are injected at a later step." + exit 1 build: needs: unit-test runs-on: ubuntu-latest diff --git a/templates/package.json b/templates/package.json index e63d1a5..a449f64 100644 --- a/templates/package.json +++ b/templates/package.json @@ -3,8 +3,10 @@ "version": "0.1.0", "private": true, "dependencies": { - "@oryd/kratos-client": "^0.5.3-alpha.1", - "@testing-library/jest-dom": "^4.2.4", + <%if eq (index .Params `userAuth`) "yes" %>"@oryd/kratos-client": "^0.5.3-alpha.1", + <% end %><%if eq (index .Params `billingEnabled`) "yes" %>"@stripe/react-stripe-js": "^1.4.0", + "@stripe/stripe-js": "^1.13.2", + <% end %>"@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", "react": "^16.13.1", diff --git a/templates/src/App.js b/templates/src/App.js index 3508379..5825af7 100644 --- a/templates/src/App.js +++ b/templates/src/App.js @@ -5,7 +5,9 @@ import Home from './pages/Home' <%if eq (index .Params `userAuth`) "yes" %> import Auth from './pages/Auth' import Logout from './pages/Logout'<% end %> - +<%if eq (index .Params `billingEnabled`) "yes" %> +import BillingRoutes from './pages/Billing/routes' +<% end %> import Dashboard from './pages/Dashboard' import PageNotFound from './pages/PageNotFound' @@ -57,7 +59,10 @@ function App() { - + <%if eq (index .Params `billingEnabled`) "yes" %> + + + <% end %> @@ -73,7 +78,10 @@ function App() { - + <%if eq (index .Params `billingEnabled`) "yes" %> + + + <% end %> diff --git a/templates/src/components/Navigation.js b/templates/src/components/Navigation.js index 974148f..2b07b78 100644 --- a/templates/src/components/Navigation.js +++ b/templates/src/components/Navigation.js @@ -14,7 +14,10 @@ function AuthenticatedLinks() {
  • User Settings
  • -
  • + <%if eq (index .Params `billingEnabled`) "yes" %>
  • + Billing +
  • + <% end %>
  • Logout
  • @@ -57,7 +60,10 @@ function NavLinks() {
  • Dashboard
  • - + <%if eq (index .Params `billingEnabled`) "yes" %>
  • + Billing +
  • + <% end %> } <% end %> function Navigation() { diff --git a/templates/src/config/development.json b/templates/src/config/development.json index d264827..de5d68e 100644 --- a/templates/src/config/development.json +++ b/templates/src/config/development.json @@ -1,5 +1,6 @@ { "appName": "<% .Name %>", "environment": "development", - "backendURL": "http://localhost:8080" + "backendURL": "http://localhost:8080"<%if eq (index .Params `billingEnabled`) "yes" %>, + "stripePublishableKey": "<% index .Params `stagingStripePublicApiKey` %>"<% end %> } diff --git a/templates/src/config/production.json b/templates/src/config/production.json index fb4a421..9e5c5f9 100644 --- a/templates/src/config/production.json +++ b/templates/src/config/production.json @@ -1,5 +1,6 @@ { "appName": "<% .Name %>", "environment": "production", - "backendURL": "https://<% index .Params `productionBackendSubdomain` %><% index .Params `productionHostRoot` %>" + "backendURL": "https://<% index .Params `productionBackendSubdomain` %><% index .Params `productionHostRoot` %>"<%if eq (index .Params `billingEnabled`) "yes" %>, + "stripePublishableKey": "<% index .Params `productionStripePublicApiKey` %>"<% end %> } diff --git a/templates/src/config/staging.json b/templates/src/config/staging.json index e018b4e..7523929 100644 --- a/templates/src/config/staging.json +++ b/templates/src/config/staging.json @@ -1,5 +1,6 @@ { "appName": "<% .Name %>", "environment": "staging", - "backendURL": "https://<% index .Params `stagingBackendSubdomain` %><% index .Params `stagingHostRoot` %>" + "backendURL": "https://<% index .Params `stagingBackendSubdomain` %><% index .Params `stagingHostRoot` %>"<%if eq (index .Params `billingEnabled`) "yes" %>, + "stripePublishableKey": "<% index .Params `stagingStripePublicApiKey` %>"<% end %> } diff --git a/templates/src/pages/Billing/Confirmation.js b/templates/src/pages/Billing/Confirmation.js new file mode 100644 index 0000000..c5a227d --- /dev/null +++ b/templates/src/pages/Billing/Confirmation.js @@ -0,0 +1,17 @@ +import React from 'react' + +import Card from '../../components/Card' + +function Confirmation() { + const queryString = decodeURIComponent(new URL(window.location).searchParams.toString()); + const queryParams = queryString.split("&"); + return ( +
    + + { queryParams.map((param) => (

    { param }

    )) } +
    +
    + ) +} + +export default Confirmation diff --git a/templates/src/pages/Billing/Plans.css b/templates/src/pages/Billing/Plans.css new file mode 100644 index 0000000..89b244b --- /dev/null +++ b/templates/src/pages/Billing/Plans.css @@ -0,0 +1,47 @@ +.billing-container .card { + min-width: auto; + max-width: 80%; +} + +.products { + text-align: center; + width: 100%; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-around; +} + +.productInfo, .billing-container button { + background: #f3f3ff; + border: 1px #ccc solid; + cursor: pointer; + padding: 3%; +} + +.productName { + font-weight: bold; +} + +.productPrice { + margin-top: 1rem; +} + +.productInfo { + min-width: 150px; + max-width: 250px; + margin: 1%; +} + +.productInfo div:not(:last-child) { + padding-bottom: 0.7rem; + margin-bottom: 0.7rem; + border-bottom: 1px solid grey; +} + +.billing-container button{ + font-weight: bold; + padding: 20px; + width: 25%; + margin: 30px auto 0; +} diff --git a/templates/src/pages/Billing/Plans.js b/templates/src/pages/Billing/Plans.js new file mode 100644 index 0000000..1da9a24 --- /dev/null +++ b/templates/src/pages/Billing/Plans.js @@ -0,0 +1,91 @@ +import React, { useEffect, useState } from 'react' +import config from '../../config'; +import './Plans.css' + +import Card from '../../components/Card' +import { useStripe } from '@stripe/react-stripe-js'; + +function Plans() { + const stripe = useStripe() + const [data, setData] = useState({ + isLoading: true, + products: [], + }) + const [selected, setSelected] = useState(null); + + const getProducts = async() => { + const uri = `${config.backendURL}/billing/products`; + const resp = await fetch(uri,{ credentials : "include" }); + if (resp.status !== 200 ) { + // error case, should show error + return setData({ isLoading: false, products: [] }); + } + const data = await resp.json(); + setData({ isLoading: false, products: data }); + }; + + const checkout = async(data) => { + if ( !data.price_id ) return null; + const uri = `${config.backendURL}/billing/checkout?price_id=${data.price_id}`; + const resp = await fetch(uri, { + credentials: "include", + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(data), + }); + const { sessionId } = await resp.json(); + stripe.redirectToCheckout({ sessionId }); + } + + const handleSubmit = (e) => { + e.preventDefault(); + checkout({ price_id: selected }); + return false; + }; + + const Product = ({ product }) => ( + ) + + const Products = ({ products }) => { + if (products.length === 0) { + return
    No products
    + } + return ( +
    +
    + { products.map(i => ) } +
    + +
    + )} + + useEffect(() => { + getProducts(); + }, []); + + return ( +
    + + { data.isLoading ? +

    Loading

    : + + } +
    +
    + ) +} + +export default Plans diff --git a/templates/src/pages/Billing/routes.js b/templates/src/pages/Billing/routes.js new file mode 100644 index 0000000..3d2a0af --- /dev/null +++ b/templates/src/pages/Billing/routes.js @@ -0,0 +1,23 @@ +import React from 'react' +import { BrowserRouter as Switch, Route } from 'react-router-dom' +import { loadStripe } from '@stripe/stripe-js'; +import { Elements} from '@stripe/react-stripe-js'; + +import config from '../../config'; +import Plans from './Plans' +import Confirmation from './Confirmation' + +const stripePromise = loadStripe(config.stripePublishableKey); + +function Billing () { + return ( + + + + + + + ) +} + +export default Billing diff --git a/zero-module.yml b/zero-module.yml index 4a2ff99..e43ac8e 100644 --- a/zero-module.yml +++ b/zero-module.yml @@ -119,7 +119,35 @@ parameters: - action: KeyMatchCondition matchField: CIVendor whenValue: "circleci" - + - field: billingEnabled + label: "Provides a subscription example using stripe in backend and frontend repository" + options: + "yes": "Yes" + "no": "No" + - field: stagingStripePublicApiKey + label: "Staging Stripe public api key, used for frontend repository (Recommended: using sandbox key while setting up)" + conditions: + - action: KeyMatchCondition + matchField: billingEnabled + whenValue: "yes" + - field: stagingStripeSecretApiKey + label: "Staging Stripe secret api key, used for backend repository (Recommended: using sandbox key while setting up)" + conditions: + - action: KeyMatchCondition + matchField: billingEnabled + whenValue: "yes" + - field: productionStripePublicApiKey + label: "Production Stripe public api key, used for frontend repository (Recommended: using sandbox key while setting up)" + conditions: + - action: KeyMatchCondition + matchField: billingEnabled + whenValue: "yes" + - field: productionStripeSecretApiKey + label: "Production Stripe secret api key, used for backend repository (Recommended: using sandbox key while setting up)" + conditions: + - action: KeyMatchCondition + matchField: billingEnabled + whenValue: "yes" conditions: - action: ignoreFile matchField: userAuth From f487f7d47e8dd648c9a8f47be136a698d37aba41 Mon Sep 17 00:00:00 2001 From: David Cheung Date: Fri, 30 Apr 2021 13:11:25 -0400 Subject: [PATCH 2/3] fixup! stripe initial impl --- templates/README.md | 18 ++++++++++++++++++ zero-module.yml | 5 +++++ 2 files changed, 23 insertions(+) diff --git a/templates/README.md b/templates/README.md index cb35c38..a0a33aa 100644 --- a/templates/README.md +++ b/templates/README.md @@ -64,3 +64,21 @@ REACT_APP_CONFIG=staging yarn build serve -s build ``` + +<%if eq (index .Params `billingEnabled`) "yes" %> +## Billing example +A subscription and checkout example using [Stripe](https://stripe.com), coupled with the backend repository to provide an end-to-end checkout example for you to customize. We also setup a webhook and an endpoint in the backend to receive webhook when events occur. + +### Setup +We have setup for you in the stripe platform +- 1 product +- 3 prices(subscriptions) [annual, monthly, daily] +- 1 webhook [`charge.failed`, `charge.succeeded`, `customer.created`, `subscription_schedule.created`] +See link for available webhooks: https://stripe.com/docs/api/webhook_endpoints/create?lang=curl#create_webhook_endpoint-enabled_events + +this is setup using the script [scripts/stripe-example-setup.sh](scripts/stripe-example-setup.sh) + +### Deployment +The deployment requires the publishable key in the build, per environment you will have to provide `stripePublishableKey` in `config/.json` [`production`/`staging`/`development`], then when CI builds it will create a bundle with the Stripe publishable API key + +<% end %> \ No newline at end of file diff --git a/zero-module.yml b/zero-module.yml index e43ac8e..5e339eb 100644 --- a/zero-module.yml +++ b/zero-module.yml @@ -171,3 +171,8 @@ conditions: whenValue: "github-actions" data: - .circleci/ + - action: ignoreFile + matchField: CIVendor + whenValue: "github-actions" + data: + - src/pages/Billing From f7857fc0334b0cc60ded87807d75ff592e54e9fa Mon Sep 17 00:00:00 2001 From: David Cheung Date: Fri, 30 Apr 2021 17:30:31 -0400 Subject: [PATCH 3/3] fixup! fixup! stripe initial impl --- templates/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/README.md b/templates/README.md index a0a33aa..1b7659c 100644 --- a/templates/README.md +++ b/templates/README.md @@ -70,7 +70,7 @@ serve -s build A subscription and checkout example using [Stripe](https://stripe.com), coupled with the backend repository to provide an end-to-end checkout example for you to customize. We also setup a webhook and an endpoint in the backend to receive webhook when events occur. ### Setup -We have setup for you in the stripe platform +The following example content has been set up in Stripe: - 1 product - 3 prices(subscriptions) [annual, monthly, daily] - 1 webhook [`charge.failed`, `charge.succeeded`, `customer.created`, `subscription_schedule.created`]