braintree.plugin.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. import { LanguageCode, PluginCommonModule, Type, VendurePlugin } from '@vendure/core';
  2. import { gql } from 'graphql-tag';
  3. import { braintreePaymentMethodHandler } from './braintree.handler';
  4. import { BraintreeResolver } from './braintree.resolver';
  5. import { BRAINTREE_PLUGIN_OPTIONS } from './constants';
  6. import { BraintreePluginOptions } from './types';
  7. /**
  8. * @description
  9. * This plugin enables payments to be processed by [Braintree](https://www.braintreepayments.com/), a popular payment provider.
  10. *
  11. * ## Requirements
  12. *
  13. * 1. You will need to create a Braintree sandbox account as outlined in https://developers.braintreepayments.com/start/overview.
  14. * 2. Then install `braintree` and `@types/braintree` from npm. This plugin was written with `v3.x` of the Braintree lib.
  15. * ```shell
  16. * yarn add \@vendure/payments-plugin braintree
  17. * yarn add -D \@types/braintree
  18. * ```
  19. * or
  20. * ```shell
  21. * npm install \@vendure/payments-plugin braintree
  22. * npm install -D \@types/braintree
  23. * ```
  24. *
  25. * ## Setup
  26. *
  27. * 1. Add the plugin to your VendureConfig `plugins` array:
  28. * ```ts
  29. * import { BraintreePlugin } from '\@vendure/payments-plugin/package/braintree';
  30. * import { Environment } from 'braintree';
  31. *
  32. * // ...
  33. *
  34. * plugins: [
  35. * BraintreePlugin.init({
  36. * environment: Environment.Sandbox,
  37. * // This allows saving customer payment
  38. * // methods with Braintree (see "vaulting"
  39. * // section below for details)
  40. * storeCustomersInBraintree: true,
  41. * }),
  42. * ]
  43. * ```
  44. * 2. Create a new PaymentMethod in the Admin UI, and select "Braintree payments" as the handler.
  45. * 2. Fill in the `Merchant ID`, `Public Key` & `Private Key` from your Braintree sandbox account.
  46. *
  47. * ## Storefront usage
  48. *
  49. * The plugin is designed to work with the [Braintree drop-in UI](https://developers.braintreepayments.com/guides/drop-in/overview/javascript/v3).
  50. * This is a library provided by Braintree which will handle the payment UI for you. You can install it in your storefront project
  51. * with:
  52. *
  53. * ```shell
  54. * yarn add braintree-web-drop-in
  55. * # or
  56. * npm install braintree-web-drop-in
  57. * ```
  58. *
  59. * The high-level workflow is:
  60. * 1. Generate a "client token" on the server by executing the `generateBraintreeClientToken` mutation which is exposed by this plugin.
  61. * 2. Use this client token to instantiate the Braintree Dropin UI.
  62. * 3. Listen for the `"paymentMethodRequestable"` event which emitted by the Dropin.
  63. * 4. Use the Dropin's `requestPaymentMethod()` method to get the required payment metadata.
  64. * 5. Pass that metadata to the `addPaymentToOrder` mutation. The metadata should be an object of type `{ nonce: string; }`
  65. *
  66. * Here is an example of how your storefront code will look. Note that this example is attempting to
  67. * be framework-agnostic, so you'll need to adapt it to fit to your framework of choice.
  68. *
  69. * ```ts
  70. * // The Braintree Dropin instance
  71. * let dropin: import('braintree-web-drop-in').Dropin;
  72. *
  73. * // Used to show/hide a "submit" button, which would be bound to the
  74. * // `submitPayment()` method below.
  75. * let showSubmitButton = false;
  76. *
  77. * // Used to display a "processing..." spinner
  78. * let processing = false;
  79. *
  80. * //
  81. * // This method would be invoked when the payment screen is mounted/created.
  82. * //
  83. * async function renderDropin(order: Order, clientToken: string) {
  84. * // Lazy load braintree dropin because it has a reference
  85. * // to `window` which breaks SSR
  86. * dropin = await import('braintree-web-drop-in').then((module) =>
  87. * module.default.create({
  88. * authorization: clientToken,
  89. * // This assumes a div in your view with the corresponding ID
  90. * container: '#dropin-container',
  91. * card: {
  92. * cardholderName: {
  93. * required: true,
  94. * },
  95. * overrides: {},
  96. * },
  97. * // Additional config is passed here depending on
  98. * // which payment methods you have enabled in your
  99. * // Braintree account.
  100. * paypal: {
  101. * flow: 'checkout',
  102. * amount: order.totalWithTax / 100,
  103. * currency: 'GBP',
  104. * },
  105. * }),
  106. * );
  107. *
  108. * // If you are using the `storeCustomersInBraintree` option, then the
  109. * // customer might already have a stored payment method selected as
  110. * // soon as the dropin script loads. In this case, show the submit
  111. * // button immediately.
  112. * if (dropin.isPaymentMethodRequestable()) {
  113. * showSubmitButton = true;
  114. * }
  115. *
  116. * dropin.on('paymentMethodRequestable', (payload) => {
  117. * if (payload.type === 'CreditCard') {
  118. * showSubmitButton = true;
  119. * }
  120. * if (payload.type === 'PayPalAccount') {
  121. * this.submitPayment();
  122. * }
  123. * });
  124. *
  125. * dropin.on('noPaymentMethodRequestable', () => {
  126. * // Display an error
  127. * });
  128. * }
  129. *
  130. * async function generateClientToken() {
  131. * const { generateBraintreeClientToken } = await graphQlClient.query(gql`
  132. * query GenerateBraintreeClientToken {
  133. * generateBraintreeClientToken
  134. * }
  135. * `);
  136. * return generateBraintreeClientToken;
  137. * }
  138. *
  139. * async submitPayment() {
  140. * if (!dropin.isPaymentMethodRequestable()) {
  141. * return;
  142. * }
  143. * showSubmitButton = false;
  144. * processing = true;
  145. *
  146. * const paymentResult = await dropin.requestPaymentMethod();
  147. *
  148. * const { addPaymentToOrder } = await graphQlClient.query(gql`
  149. * mutation AddPayment($input: PaymentInput!) {
  150. * addPaymentToOrder(input: $input) {
  151. * ... on Order {
  152. * id
  153. * payments {
  154. * id
  155. * amount
  156. * errorMessage
  157. * method
  158. * state
  159. * transactionId
  160. * createdAt
  161. * }
  162. * }
  163. * ... on ErrorResult {
  164. * errorCode
  165. * message
  166. * }
  167. * }
  168. * }`, {
  169. * input: {
  170. * method: 'braintree', // The code of you Braintree PaymentMethod
  171. * metadata: paymentResult,
  172. * },
  173. * },
  174. * );
  175. *
  176. * switch (addPaymentToOrder?.__typename) {
  177. * case 'Order':
  178. * // Adding payment succeeded!
  179. * break;
  180. * case 'OrderStateTransitionError':
  181. * case 'OrderPaymentStateError':
  182. * case 'PaymentDeclinedError':
  183. * case 'PaymentFailedError':
  184. * // Display an error to the customer
  185. * dropin.clearSelectedPaymentMethod();
  186. * }
  187. * }
  188. * ```
  189. *
  190. * ## Storing payment details (vaulting)
  191. *
  192. * Braintree has a [vault feature](https://developer.paypal.com/braintree/articles/control-panel/vault/overview) which allows the secure storage
  193. * of customer's payment information. Using the vault allows you to offer a faster checkout for repeat customers without needing to worry about
  194. * how to securely store payment details.
  195. *
  196. * To enable this feature, set the `storeCustomersInBraintree` option to `true`.
  197. *
  198. * ```ts
  199. * BraintreePlugin.init({
  200. * environment: Environment.Sandbox,
  201. * storeCustomersInBraintree: true,
  202. * }),
  203. * ```
  204. *
  205. * Since v1.8, it is possible to override vaulting on a per-payment basis by passing `includeCustomerId: false` to the `generateBraintreeClientToken`
  206. * mutation:
  207. *
  208. * ```GraphQL
  209. * const { generateBraintreeClientToken } = await graphQlClient.query(gql`
  210. * query GenerateBraintreeClientToken($includeCustomerId: Boolean) {
  211. * generateBraintreeClientToken(includeCustomerId: $includeCustomerId)
  212. * }
  213. * `, { includeCustomerId: false });
  214. * ```
  215. *
  216. * as well as in the metadata of the `addPaymentToOrder` mutation:
  217. *
  218. * ```ts
  219. * const { addPaymentToOrder } = await graphQlClient.query(gql`
  220. * mutation AddPayment($input: PaymentInput!) {
  221. * addPaymentToOrder(input: $input) {
  222. * ...Order
  223. * ...ErrorResult
  224. * }
  225. * }`, {
  226. * input: {
  227. * method: 'braintree',
  228. * metadata: {
  229. * ...paymentResult,
  230. * includeCustomerId: false,
  231. * },
  232. * }
  233. * );
  234. * ```
  235. *
  236. * @docsCategory core plugins/PaymentsPlugin
  237. * @docsPage BraintreePlugin
  238. */
  239. @VendurePlugin({
  240. imports: [PluginCommonModule],
  241. providers: [
  242. {
  243. provide: BRAINTREE_PLUGIN_OPTIONS,
  244. useFactory: () => BraintreePlugin.options,
  245. },
  246. ],
  247. configuration: config => {
  248. config.paymentOptions.paymentMethodHandlers.push(braintreePaymentMethodHandler);
  249. if (BraintreePlugin.options.storeCustomersInBraintree === true) {
  250. config.customFields.Customer.push({
  251. name: 'braintreeCustomerId',
  252. type: 'string',
  253. label: [{ languageCode: LanguageCode.en, value: 'Braintree Customer ID' }],
  254. nullable: true,
  255. public: false,
  256. readonly: true,
  257. });
  258. }
  259. return config;
  260. },
  261. shopApiExtensions: {
  262. schema: gql`
  263. extend type Query {
  264. generateBraintreeClientToken(orderId: ID, includeCustomerId: Boolean): String!
  265. }
  266. `,
  267. resolvers: [BraintreeResolver],
  268. },
  269. compatibility: '^2.0.0',
  270. })
  271. export class BraintreePlugin {
  272. static options: BraintreePluginOptions = {};
  273. static init(options: BraintreePluginOptions): Type<BraintreePlugin> {
  274. this.options = options;
  275. return BraintreePlugin;
  276. }
  277. }