Stripe Subscription Manager
Pylon is a stripe subscription manager.
User Guide
Create products
First, create a subscription production in https://dashboard.stripe.com/products.
Setup product and webhook in Pylon
For test
- Get api keys and add to config/stripe-config.js
- Get webhook secret and add to config/stripe-config.js
- specify prices in src/shared/home/home.tsx
- register webhook to localhost:4108/pylon/api/webhook/:clientId
When registering webhook, add these events to send
- customer.created
- customer.updated
- invoice.upcoming
- invoice.created
- invoice.finalized
- invoice.payment_failed
- invoice.payment_succeeded
- customer.subscription.created
- customer.subscription.updated
- customer.subscription.deleted
- checkout.session.completed
Merge your schema from your product
Once the Pylon server is deployed, you could connect to it with merging schema
import {ApolloServer, introspectSchema, makeRemoteExecutableSchema, mergeSchemas} from "apollo-server-koa";
import { GraphQLSchema, printSchema } from "graphql";
import { HttpLink } from "apollo-link-http";
import { setContext } from "apollo-link-context";
import {logger} from "onefx/lib/integrated-gateways/logger";
import config from "config";
import { GraphQLRequest } from "apollo-link";
import fs from "fs";
async function safeAppendSchema(schemas: Array<GraphQLSchema>) {
try {
const remoteLink = setContext((_: GraphQLRequest, prevContext: any) => {
const auth = prevContext?.graphqlContext?.headers?.authorization;
return {
headers: {
Authorization: auth,
},
};
}).concat(
new HttpLink({
uri: "https://stargately.com/pylon/api-gateway/",
fetch,
headers: {
"x-client-id": config.get("project"),
"x-tenant-id": "prod",
},
})
);
const remoteSchema = makeRemoteExecutableSchema({
schema: await introspectSchema(remoteLink),
link: remoteLink,
});
schemas.push(remoteSchema);
} catch (e) {
logger.error(`failed to append schema: ${e}`);
}
}
Update local schema to merge remote one, and emit the new schema file
const schemas = [await buildSchema({
resolvers,
authChecker: customAuthChecker,
validate: false,
})];
await safeAppendSchema(schemas);
const schema = mergeSchemas({
schemas
});
fs.writeFileSync(sdlPath, printSchema(schema));
Mount the checkout button in your product
Get your pricing plan Id from https://dashboard.stripe.com/products
and put that into your product. Here is an example with React.
<CheckoutButton
userId={"userId"}
clientId={"clientId"}
priceId={"price_example"}
email={"email@example.com"}
pubKey={"pk_example"}
>
Buy
</CheckoutButton>
Here is an example of CheckoutButton:
.
├── checkout-button.tsx
├── data
│ ├── __generated__
│ │ └── CreateSubscriptionSession.ts
│ └── mutations.ts
└── hooks
└── use-create-subscription-session.ts
checkout-button.tsx
import React from "react";
import Button from "antd/lib/button";
import notification from "antd/lib/notification";
import { useCreateSubscriptionSession } from "./hooks/use-create-subscription-session";
const loadScript = require("load-script");
const window = require("global/window");
function lazyLoadStripe(pubKey: string, cb: (stripe: any) => void): void {
if (window.stripe) {
cb(window.stripe);
return;
}
loadScript("https://js.stripe.com/v3/", () => {
window.stripe = window.Stripe(pubKey);
cb(window.stripe);
});
}
type Props = {
userId: string;
priceId: string;
pubKey: string;
clientId: string;
email?: string;
children: JSX.Element | string | React.ReactNode;
};
export function CheckoutButton({
userId,
priceId,
email,
pubKey,
clientId,
children,
}: Props): JSX.Element {
const { createSubscriptionSession, loading } = useCreateSubscriptionSession();
const handleClick = async () => {
lazyLoadStripe(pubKey, async (stripe) => {
const resp = await createSubscriptionSession({
variables: {
userId,
priceId,
email,
clientId,
},
});
const result = await stripe.redirectToCheckout({
sessionId: resp.data?.createSubscriptionSession.id,
});
if (result.error) {
notification.error({ message: `failed to checkout: ${result.error}` });
}
});
};
return (
<Button role="link" onClick={handleClick} loading={loading}>
{children}
</Button>
);
}
data/mutations.ts
import { gql } from "@apollo/client";
export const createSubscriptionSession = gql`
mutation CreateSubscriptionSession(
$clientId: String!
$userId: String!
$email: String
$priceId: String!
$coupon: String
) {
createSubscriptionSession(
userId: $userId
email: $email
priceId: $priceId
coupon: $coupon
clientId: $clientId
) {
id
}
}
`;
data/__generated__/CreateSubscriptionSession.ts
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL mutation operation: CreateSubscriptionSession
// ====================================================
export interface CreateSubscriptionSession_createSubscriptionSession {
id: string;
}
export interface CreateSubscriptionSession {
/**
* starts a subscription session for userId <> clientId <> priceId
*/
createSubscriptionSession: CreateSubscriptionSession_createSubscriptionSession;
}
export interface CreateSubscriptionSessionVariables {
clientId: string;
userId: string;
email?: string | null;
priceId: string;
coupon?: string | null;
}
hooks/use-create-subscription-session.ts
import { useMutation } from "@apollo/client";
import { createSubscriptionSession } from "../data/mutations";
import { CreateSubscriptionSession } from "../data/__generated__/CreateSubscriptionSession";
export const useCreateSubscriptionSession = () => {
const [mutate, { data, loading }] = useMutation<CreateSubscriptionSession>(
createSubscriptionSession
);
return {
createSubscriptionSession: mutate,
createdSession: data?.createSubscriptionSession,
loading,
};
};
query PaymentSubscription
query PaymentSubscription($clientId: String!, $userId: String!) {
paymentSubscription(clientId: $clientId, userId: $userId) {
_id
userId
status
clientId
status
stripe {
planId
subscriptionId
productId
subscriptionStatus
customerId
}
paymentMethod
nextBillingDate
currentPeriodEnd
currentPeriodStart
cancelAt
}
}
{
"userId": "",
"clientId": ""
}
cancel subscription
mutation CancelSubscription($clientId: String!, $userId: String!) {
cancelSubscription(clientId: $clientId, userId: $userId) {
ok
}
}
{
"userId": "",
"clientId": ""
}