Payments

This package helps you process charges with Vulcan. It currently only supports Stripe, but other payment processors may be supported in the future (PRs welcome!).

Overview

This package does the following things:

  • Provide a button that triggers the Stripe Checkout form.
  • Once the form is submitted, trigger a GraphQL mutation that will:
    • Perform the charge.
    • Create a new Charge.
    • Modify a document associated with the charge.
  • The mutation then returns a document associated with the charge to the client.

Install

1
yarn add stripe react-stripe-checkout

Settings

Stripe requires the following public setting in your settings.json.

  • public.stripe.publishableKey: your publishable Stripe key.

As well as the following private setting (can be stored in the setting’s root or on private):

  • stripe.secretKey: your Stripe secret key.
1
2
3
4
5
6
7
8
9
10
11
{
"public": {
"stripe": {
"publishableKey": "pk_test_K0rkFDrT0jj4NqG5Dumr3RaU"
}
}
},
"stripe": {
"secretKey": "sk_test_sfdhj34jdsfxhjs234sd0K"
},
}

Charges

Charges are stored in the database with the following fields:

  • _id: the charge’s id.
  • createdAt: the charge’s timestamp.
  • userId: the Vulcan _id of the user performing the purchase.
  • tokenId: the charge token’s id.
  • productKey: the key corresponding to the product being purchased, as defined with addProduct.
  • type: the type of charge (currently only stripe is supported).
  • test: whether the operation is a test or not.
  • data: a JSON object containing all charge data generated by the payment processor.
  • properties: a JSON object containing any custom properties passed by the client.
  • ip: the IP address of the client performing the purchase.

Products

A product is a type of purchase a user can make. It has a name, amount (in cents), currency, and description.

New products are defined using the addProduct function, which takes two arguments. The first argument is a unique product key used to identify the product. The second argument can be an object (for “static” products like a subscription):

1
2
3
4
5
6
7
8
import { addProduct } from 'meteor/vulcan:payments';

addProduct('membership', {
name: 'VulcanJS Membership',
amount: 25000,
currency: 'USD',
description: 'Become a paid member.'
});

Or it can be a function (for “dynamic” products like in an e-commerce site) that takes the associated document (i.e. the product being sold) as argument and returns an object:

1
2
3
4
5
6
7
8
import { addProduct } from 'meteor/vulcan:payments';

addProduct('book', book => ({
name: book.title,
amount: book.price,
currency: 'USD',
description: book.description
}));

Coupons

In addition, you can also define coupon codes for your products:

1
2
3
4
5
6
7
8
addProduct('membership', {
amount: 25000,
currency: 'USD',
description: 'Become a paid member.',
coupons: {
VULCAN: 10000,
}
});

Make sure you define your products in a location accessible to both client and server, in order to access them both on the front-end to configure Stripe Checkout, and in the back-end to perform the actual charge.

Subscription Products

You can create recurring subscription products:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
addProduct('monthlyBooking', booking => {

// TODO: don't use findOne
const room = booking.room || Rooms.findOne({_id: booking.roomId});

return {
name: 'Rent this room',
currency: 'JPY',
description: `Rent ${room.name}`,
plan: getPlanId(room, booking.numberOfGuests),
initialAmount: booking.initialAmount,
initialAmountDescription: `Security deposit and cleaning fee`,
amount: booking.amount,
subscriptionProperties: {
billing_cycle_anchor: booking.startAt.valueOf().toString().slice(0,-3)
}
}
});

Subscription Properties

You can add a subscriptionProperties object to a product to pass extra properties to the payment gateway. For example, here’s how to delay a subscription until the start date of a booking by setting the subscription’s trial_end property:

1
2
3
4
5
6
7
8
9
10
addProduct('booking', booking => {
return {
name: 'Rent a room',
currency: 'JPY',
amount: booking.amount,
subscriptionProperties: {
trial_end: booking.startAt.valueOf().toString().slice(0,-3)
}
}
});

Inside vulcan:payments, this subscriptionProperties object will be passed on to the following code:

1
2
3
4
5
6
7
8
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [
{ plan: product.plan },
],
metadata,
...product.subscriptionProperties,
});

Checkout Component

1
2
3
4
5
6
7
<Components.Checkout 
productKey="jobPosting"
associatedCollection={Jobs}
associatedDocument={job}
callback={setToPaid}
button={<Button className="buy-job-button" bsStyle="primary">Complete Payment</Button>}
/>
  • productKey: The key of the product to buy.
  • button: The button that triggers the Stripe Checkout overlay.
  • associatedCollection: the associated collection.
  • associatedDocument: the associated document.
  • callback: a callback function that runs once the charge is successful (takes the charge as result argument).
  • fragment: a GraphQL fragment specifying the fields expected in return after the charge.
  • fragmentName: a registeredGraphQL fragment name.
  • properties: any other properties you want to pass on to createChargeMutation on the server.

Associating a Collection Document

The Vulcan Charge package requires associating a document with a purchase, typically the item being paid for. For example, maybe you want people to buy access to a file hosted on your servers, and give them download access once the transaction is complete.

The associatedCollection and associatedId props give you an easy way to implement this by automatically setting a chargeIds field on the document once the charge succeeds.

For example, if you pass associatedCollection={Jobs} and associatedId="foo123" to the Checkout component, the resulting charge’s _id will automatically be added to a chargeIds array on job foo123.

The createChargeMutation GraphQL mutation will then return that job according to the fragment property specified.

Note: you will need to make sure that your collection accepts this chargeIds field. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Jobs.addField([
{
fieldName: 'chargeIds',
fieldSchema: {
type: Array,
optional: true,
}
},
{
fieldName: 'chargeIds.$',
fieldSchema: {
type: String,
optional: true,
}
}
]);

The “Chargeable” Type

In order to be able to return any associated document, the package creates a new Chargeable GraphQL type that is an union of every collection’s types.

Post-Charge Updates

The best way to update a document based on a successful charge is by using the collection.charge.sync callback.

Callback functions on this hook will run with a MongoDB modifier as the first argument (although note that only $set and $unset operations are supported here), the document associated with the charge as their second argument, and the charge object as their third argument.

Because the callback is added in a sync manner, the final document returned by the createChargeMutation mutation will include any new values set by the callback hook.

Example 1: Setting a job offer as paid

1
2
3
4
5
6
7
8
import { addCallback } from 'meteor/vulcan:core';

function setToPaidOnCharge (modifier, job, charge) {
modifier.$set.status = 'paid';
return modifier;
}

addCallback('jobs.charge.sync', setToPaidOnCharge);

Example 2: Adding a user to a group

1
2
3
4
5
6
7
8
import { addCallback } from 'meteor/vulcan:core';

function makePaidMember (modifier, user, charge) {
modifier.$set.groups = [...user.groups, 'paidMembers'];
return modifier;
}

addCallback('users.charge.sync', makePaidMember);

Example 3: Giving a user access to a specific document

We’ll pass the videoId property to our Checkout component (property={ { videoId: video._id } }) to make it accessible as charge.properties.videoId inside the callback:

1
2
3
4
5
6
7
8
9
import { addCallback } from 'meteor/vulcan:core';

function giveAccessToVideo (modifier, user, charge) {
const videoId = charge.properties.videoId;
modifier.$set.accessibleVideos = [...user.accessibleVideos, videoId];
return modifier;
}

addCallback('users.charge.sync', giveAccessToVideo);
Edit on GitHub