Search code examples
typescriptstripe-paymentsnativescriptnativescript-vuenativescript-plugin

How to setup ‘nativescript-stripe’ in NativeScript Vue


I’m trying to set up the «nativescript-stripe» plugin in my Nativescript Vue app. Following the demos on the plugins github is a bit difficult as they only have demos for Angular and TypeScript. Has anyone gotten the StandardComponent to work with Vue and can tell me where and what parameters to send in to StripeService.createPaymentSession()?

I tried setting <Page ref=«cartPage»> in the template and on mounted() setting paymentSession:

import {
  StripeAddress,
  StripePaymentData,
  StripePaymentListener,
  StripePaymentMethod,
  StripePaymentSession,
  StripeShippingMethod,
  StripeShippingMethods
} from "nativescript-stripe/standard";
import { StripeService, Listener } from "~/shared/stripe/stripe.service";

var paymentSession = {};

export default {
  mounted() {
    //not sure if this is the way to do it
    paymentSession = StripeService.createPaymentSession(
      this.$refs.cartPage,
      this.stripeItem.price,
      new Listener(this)
    );
  },

In my stripe.service.ts file, I got the same code as the Angular demo (https://github.com/triniwiz/nativescript-stripe/blob/master/demo-angular/app/demo/stripe.service.ts), except I have set the publishableKey and backendBaseURL, AND exporting a class for Listener also:

export class Listener {
  public component;
  constructor(component) {
      this.component = component;
  }
  onCommunicatingStateChanged(_isCommunicating) {
      this.component.changeDetectionRef.detectChanges();
  }
//etc. (code from https://github.com/triniwiz/nativescript-stripe/blob/master/demo-angular/app/demo/standard.component.ts)

I think maybe I should move the Listener class to it's own file also, but don't belive that's the problem right now.

The app crashes with the error message:

CONSOLE ERROR file:///node_modules/nativescript-vue/dist/index.js:2129:21 [Vue warn]: Error in mounted hook: "TypeError: _shared_stripe_stripe_service__WEBPACK_IMPORTED_MODULE_6__["StripeService"].createPaymentSession is not a function. (In '_shared_stripe_stripe_service__WEBPACK_IMPORTED_MODULE_6__["StripeService"].createPaymentSession(this.$refs.cartPage, this.stripeItem.price, new _shared_stripe_stripe_service__WEBPACK_IMPORTED_MODULE_6__"Listener")', '_shared_stripe_stripe_service__WEBPACK_IMPORTED_MODULE_6__["StripeService"].createPaymentSession' is undefined)"

EDIT:

I was finally able to run the app with this setup:

ShoppingCart.vue:

<template>
  <Page ref="cartPage" class="page">
    <ActionBar class="action-bar">
      <NavigationButton ios:visibility="collapsed" icon="res://menu" @tap="onDrawerButtonTap"></NavigationButton>
      <ActionItem
        icon="res://navigation/menu"
        android:visibility="collapsed"
        @tap="onDrawerButtonTap"
        ios.position="left"
      ></ActionItem>
      <Label class="action-bar-title" text="ShoppingCart"></Label>
    </ActionBar>
    <StackLayout class="page p-10">
              <GridLayout rows="auto" columns="auto,*">
                <Label row="0" col="0" :text="stripeItem.name" class="h2"></Label>
                <Label
                  row="0"
                  col="1"
                  :text="'kr' + stripeItem.price"
                  class="text-right text-muted"
                ></Label>
              </GridLayout>
              <StackLayout class="hr-light m-10"></StackLayout>
              <GridLayout
                rows="auto"
                columns="*,auto"
                @tap="showPaymentMethods()"
                class="list-group-item"
              >
                <Label row="0" col="0" text="Payment Type"></Label>
                <StackLayout row="0" col="1" orientation="horizontal">
                  <Image width="32" height="20" :src="paymentImage"></Image>
                  <Label
                    :text="paymentType"
                    class="text-right text-muted"
                    :visibility="!isLoading ? 'visible' : 'collapse'"
                  ></Label>
                </StackLayout>
                <ActivityIndicator
                  row="0"
                  col="1"
                  :busy="isLoading"
                  :visibility="isLoading ? 'visible' : 'collapse'"
                ></ActivityIndicator>
              </GridLayout>
              <StackLayout class="hr-light m-10"></StackLayout>
              <GridLayout rows="auto" columns="auto,*" @tap="showShipping()" class="list-group-item">
                <Label row="0" col="0" text="Shipping Method"></Label>
                <Label row="0" col="1" :text="shippingType" class="text-right text-muted"></Label>
              </GridLayout>
              <StackLayout class="hr-light m-10"></StackLayout>
              <GridLayout rows="auto" columns="auto,*" class="list-group-item">
                <Label row="0" col="0" text="Total"></Label>
                <Label row="0" col="1" :text="'kr ' + total" class="text-right"></Label>
              </GridLayout>
              <StackLayout class="hr-light m-10"></StackLayout>
              <Label :text="errorMessage" class="text-danger" textWrap="true"></Label>
              <Button text="Buy" :isEnabled="canBuy" class="btn btn-primary btn-active" @tap="buy()"></Button>
              <ActivityIndicator
                :busy="paymentInProgress"
                :visibility="paymentInProgress ? 'visible' : 'collapse'"
              ></ActivityIndicator>
              <Label :text="successMessage" class="text-primary" textWrap="true"></Label>
              <StackLayout class="hr-light m-10"></StackLayout>
              <Label text="Debug Info"></Label>
              <Label :text="debugInfo" class="body" textWrap="true"></Label>
            </StackLayout>
  </Page>
</template>

<script>
import * as utils from "~/shared/utils";
import SelectedPageService from "../shared/selected-page-service";
import { StripeService, Listener } from "~/shared/stripe/stripe.service.ts";

const Page = require("tns-core-modules/ui/page").Page;

let stripeService = new StripeService();
var paymentSession = {};

export default {
  mounted() {
    SelectedPageService.getInstance().updateSelectedPage("ShoppingCart");

    paymentSession = stripeService.createPaymentSession(new Page(), 1213, new Listener(this));
  },
  data() {
    return {
      stripeItem: {
        id: 0,
        name: "Something to buy",
        price: 1200
      },
      paymentInProgress: false,
      canBuy: true,
      isLoading: false,
      paymentType: "",
      paymentImage: "",
      shippingType: "",
      total: "",
      debugInfo: "",
      successMessage: "",
      errorMessage: ""
    };
  },
  methods: {
    onDrawerButtonTap() {
      utils.showDrawer();
    },
    showPaymentMethods() {
      return stripeService.showPaymentMethods(paymentSession);
    },
    showShipping() {
      return stripeService.showShipping(paymentSession);
    },
    buy() {
      this.paymentInProgress = true;
      this.canBuy = false;
      return stripeService.requestPayment(paymentSession);
    }
  }
};
</script>

stripe.service.ts:

import { StripeAddress, StripeBackendAPI, StripeConfig, StripeCustomerSession, StripePaymentListener, StripePaymentSession, StripeShippingAddressField, StripeShippingMethod } from "nativescript-stripe/standard";
import * as httpModule from "tns-core-modules/http";
import { Page } from "tns-core-modules/ui/page";

export const publishableKey = "pk_test_xxxxremovedxxxx";
const backendBaseURL = "https://xxxxremovedxxxx.herokuapp.com/";
const appleMerchantID = "";

export class Listener {

  public component;
  constructor(component) {
      this.component = component;
  }
  onCommunicatingStateChanged(_isCommunicating) {

  }
  onPaymentDataChanged(data) {
      this.component.paymentMethod = data.paymentMethod;
      this.component.shippingInfo = data.shippingInfo;
      this.component.shippingAddress = data.shippingAddress;
  }
  onPaymentSuccess() {
      this.component.successMessage =
          `Congratulations! You bought a "${this.component.item.name}" for $${this.component.item.price / 100}.`;

  }
  onUserCancelled() {
  }
  onError(_errorCode, message) {
      this.component.errorMessage = message;
  }
  provideShippingMethods(address) {
      let upsGround = {
          amount: 0,
          label: "UPS Ground",
          detail: "Arrives in 3-5 days",
          identifier: "ups_ground"
      };
      let upsWorldwide = {
          amount: 1099,
          label: "UPS Worldwide Express",
          detail: "Arrives in 1-3 days",
          identifier: "ups_worldwide"
      };
      let fedEx = {
          amount: 599,
          label: "FedEx",
          detail: "Arrives tomorrow",
          identifier: "fedex"
      };
      let methods = {};
      if (!address.country || address.country === "US") {
          methods['isValid'] = true;
          methods['validationError'] = undefined;
          methods['shippingMethods'] = [upsGround, fedEx];
          methods['selectedShippingMethod'] = fedEx;
      }
      else if (address.country === "AQ") {
          methods['isValid'] = false;
          methods['validationError'] = "We can't ship to this country.";
      }
      else {
          fedEx.amount = 2099;
          methods['isValid'] = true;
          methods['validationError'] = undefined;
          methods['shippingMethods'] = [upsWorldwide, fedEx];
          methods['selectedShippingMethod'] = fedEx;
      }
      return methods;
  }
}


export class StripeService implements StripeBackendAPI {
  private customerSession: StripeCustomerSession;

  constructor() {
    if (-1 !== publishableKey.indexOf("pk_test_yours")) {
      throw new Error("publishableKey must be changed from placeholder");
    }
    if (-1 !== backendBaseURL.indexOf("https://yours.herokuapp.com/")) {
      throw new Error("backendBaseURL must be changed from placeholder");
    }

    StripeConfig.shared().backendAPI = this;
    StripeConfig.shared().publishableKey = publishableKey;
    StripeConfig.shared().appleMerchantID = appleMerchantID;
    StripeConfig.shared().companyName = "Demo Company";
    StripeConfig.shared().requiredShippingAddressFields = [StripeShippingAddressField.PostalAddress];

    this.customerSession = new StripeCustomerSession();
  }

  private backendURL(pathComponent: string): string {
    if (!backendBaseURL) throw new Error("backendBaseURL must be set");
    if (!backendBaseURL.endsWith("/")) {
      return backendBaseURL + "/" + pathComponent;
    } else {
      return backendBaseURL + pathComponent;
    }
  }

  createCustomerKey(apiVersion: string): Promise<any> {
    let url = this.backendURL("ephemeral_keys");
    return httpModule.request({
      url: url,
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded; charset=utf-8" },
      content: "api_version=" + apiVersion
    }).then(response => {
      if (response.statusCode < 200 || response.statusCode >= 300) {
        throw new Error(response.content.toString());
      }
      return response.content.toJSON();
    });
  }

  completeCharge(stripeID: string, amount: number, shippingMethod: StripeShippingMethod, shippingAddress: StripeAddress): Promise<void> {
    let url = this.backendURL("capture_payment");
    return httpModule.request({
      url: url,
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded; charset=utf-8" },
      content:
        "source=" + stripeID +
        "&amount=" + amount +
        "&" + this.encodeShipping(shippingMethod, shippingAddress)
    }).then(response => {
      if (response.statusCode < 200 || response.statusCode >= 300) {
        throw new Error(response.content.toString());
      }
    });
  }

  private encodeShipping(method: StripeShippingMethod, address: StripeAddress): string {
    function entry(label: string, value: string): string {
      return value ? encodeURI(label) + "=" + encodeURI(value) : "";
    }
    return entry("shipping[carrier]", method.label) +
      entry("&shipping[name]", address.name) +
      entry("&shipping[address][line1]", address.line1) +
      entry("&shipping[address][line2]", address.line2) +
      entry("&shipping[address][city]", address.city) +
      entry("&shipping[address][state]", address.state) +
      entry("&shipping[address][country]", address.country) +
      entry("&shipping[address][postal_code]", address.postalCode) +
      entry("&phone", address.phone) +
      entry("&email", address.email);
  }

  createPaymentSession(page, price, listener?): StripePaymentSession {
    return new StripePaymentSession(page, this.customerSession, price, "usd", listener);
  }

  showPaymentMethods(paymentSession: StripePaymentSession) {
    paymentSession.presentPaymentMethods();
  }

  showShipping(paymentSession: StripePaymentSession) {
    paymentSession.presentShipping();
  }

  requestPayment(paymentSession: StripePaymentSession) {
    paymentSession.requestPayment();
  }
}

The issue I'm facing now (also part of the setup) is that nothing happens when I tap "Payment type". When I debug, I can see it going into the method presentPaymentMethods(). And this code from the plugin is running without any erros but nothing happens:

 StripePaymentSession.prototype.presentPaymentMethods = function () {
        this.ensureHostViewController();
        this.native.presentPaymentOptionsViewController();
    };

Anyone?


Solution

  • After many hours spent I finally figured it out. Thanks to @Manoj for the tip.

    stripe.service.ts:

    import { StripeAddress, StripeBackendAPI, StripeConfig, StripeCustomerSession, StripePaymentListener, StripePaymentSession, StripeShippingAddressField, StripeShippingMethod } from "nativescript-stripe/standard";
    import * as httpModule from "tns-core-modules/http";
    
    // 1) To get started with this demo, first head to https://dashboard.stripe.com/account/apikeys
    // and copy your "Test Publishable Key" (it looks like pk_test_abcdef) into the line below.
    export const publishableKey = "pk_test_yours";
    
    // 2) Next, optionally, to have this demo save your user's payment details, head to
    // https://github.com/stripe/example-ios-backend , click "Deploy to Heroku", and follow
    // the instructions (don't worry, it's free). Paste your Heroku URL below
    // (it looks like https://blazing-sunrise-1234.herokuapp.com ).
    const backendBaseURL = "https://yours.herokuapp.com/";
    
    // 3) Optionally, to enable Apple Pay, follow the instructions at https://stripe.com/docs/apple-pay/apps
    // to create an Apple Merchant ID. Paste it below (it looks like merchant.com.yourappname).
    const appleMerchantID = "";
    
    export class Listener {
    
      public component;
      constructor(component) {
        this.component = component;
      }
      onCommunicatingStateChanged(_isCommunicating) {
    
      }
      onPaymentDataChanged(data) {
        this.component.paymentMethod = data.paymentMethod;
        this.component.shippingInfo = data.shippingInfo;
        this.component.shippingAddress = data.shippingAddress;
      }
      onPaymentSuccess() {
        this.component.successMessage =
          `Congratulations! You bought a "${this.component.stripeItem.name}" for $${this.component.stripeItem.price / 100}.`;
    
      }
      onUserCancelled() {
      }
      onError(_errorCode, message) {
        this.component.errorMessage = message;
      }
      provideShippingMethods(address) {
        let upsGround = {
          amount: 0,
          label: "UPS Ground",
          detail: "Arrives in 3-5 days",
          identifier: "ups_ground"
        };
        let upsWorldwide = {
          amount: 1099,
          label: "UPS Worldwide Express",
          detail: "Arrives in 1-3 days",
          identifier: "ups_worldwide"
        };
        let fedEx = {
          amount: 599,
          label: "FedEx",
          detail: "Arrives tomorrow",
          identifier: "fedex"
        };
        let methods = {};
        if (!address.country || address.country === "US") {
          methods['isValid'] = true;
          methods['validationError'] = undefined;
          methods['shippingMethods'] = [upsGround, fedEx];
          methods['selectedShippingMethod'] = fedEx;
        }
        else if (address.country === "AQ") {
          methods['isValid'] = false;
          methods['validationError'] = "We can't ship to this country.";
        }
        else {
          fedEx.amount = 2099;
          methods['isValid'] = true;
          methods['validationError'] = undefined;
          methods['shippingMethods'] = [upsWorldwide, fedEx];
          methods['selectedShippingMethod'] = fedEx;
        }
        return methods;
      }
    }
    
    
    export class StripeService implements StripeBackendAPI {
      private customerSession;
    
      constructor() {
        if (-1 !== publishableKey.indexOf("pk_test_yours")) {
          throw new Error("publishableKey must be changed from placeholder");
        }
        if (-1 !== backendBaseURL.indexOf("https://yours.herokuapp.com/")) {
          throw new Error("backendBaseURL must be changed from placeholder");
        }
    
        StripeConfig.shared().backendAPI = this;
        StripeConfig.shared().publishableKey = publishableKey;
        StripeConfig.shared().appleMerchantID = appleMerchantID;
        StripeConfig.shared().companyName = "Demo Company";
        StripeConfig.shared().requiredShippingAddressFields = [StripeShippingAddressField.PostalAddress];
    
        this.customerSession = new StripeCustomerSession();
      }
    
      private backendURL(pathComponent: string): string {
        if (!backendBaseURL) throw new Error("backendBaseURL must be set");
        if (!backendBaseURL.endsWith("/")) {
          return backendBaseURL + "/" + pathComponent;
        } else {
          return backendBaseURL + pathComponent;
        }
      }
    
      createCustomerKey(apiVersion: string): Promise<any> {
        let url = this.backendURL("ephemeral_keys");
        return httpModule.request({
          url: url,
          method: "POST",
          headers: { "Content-Type": "application/x-www-form-urlencoded; charset=utf-8" },
          content: "api_version=" + apiVersion
        }).then(response => {
          if (response.statusCode < 200 || response.statusCode >= 300) {
            throw new Error(response.content.toString());
          }
          return response.content.toJSON();
        });
      }
    
      completeCharge(stripeID: string, amount: number, shippingMethod: StripeShippingMethod, shippingAddress: StripeAddress): Promise<void> {
        let url = this.backendURL("capture_payment");
        return httpModule.request({
          url: url,
          method: "POST",
          headers: { "Content-Type": "application/x-www-form-urlencoded; charset=utf-8" },
          content:
            "source=" + stripeID +
            "&amount=" + amount +
            "&" + this.encodeShipping(shippingMethod, shippingAddress)
        }).then(response => {
          if (response.statusCode < 200 || response.statusCode >= 300) {
            throw new Error(response.content.toString());
          }
        });
      }
    
      private encodeShipping(method: StripeShippingMethod, address: StripeAddress): string {
        function entry(label: string, value: string): string {
          return value ? encodeURI(label) + "=" + encodeURI(value) : "";
        }
        return entry("shipping[carrier]", method.label) +
          entry("&shipping[name]", address.name) +
          entry("&shipping[address][line1]", address.line1) +
          entry("&shipping[address][line2]", address.line2) +
          entry("&shipping[address][city]", address.city) +
          entry("&shipping[address][state]", address.state) +
          entry("&shipping[address][country]", address.country) +
          entry("&shipping[address][postal_code]", address.postalCode) +
          entry("&phone", address.phone) +
          entry("&email", address.email);
      }
    
      createPaymentSession(page, price, listener?): StripePaymentSession {
        return new StripePaymentSession(page, this.customerSession, price, "usd", listener);
      }
    
      showPaymentMethods(paymentSession: StripePaymentSession) {
        paymentSession.presentPaymentMethods();
      }
    
      showShipping(paymentSession: StripePaymentSession) {
        paymentSession.presentShipping();
      }
    
      requestPayment(paymentSession: StripePaymentSession) {
        paymentSession.requestPayment();
      }
    }
    

    Payment.vue:

    <template>
      <Page @loaded="onPageLoaded" class="page">
        <ActionBar class="action-bar">
          <Label class="action-bar-title" text="Home"></Label>
        </ActionBar>
    
        <StackLayout class="page p-10">
          <GridLayout rows="auto" columns="auto,*">
            <Label row="0" col="0" :text="stripeItem.name" class="h2"></Label>
            <Label row="0" col="1" :text="'$' + stripeItem.price" class="text-right text-muted"></Label>
          </GridLayout>
          <StackLayout class="hr-light m-10"></StackLayout>
          <GridLayout rows="auto" columns="*,auto" @tap="showPaymentMethods()" class="list-group-item">
            <Label row="0" col="0" text="Payment Type"></Label>
            <StackLayout row="0" col="1" orientation="horizontal">
              <Image width="32" height="20" :src="paymentImage"></Image>
              <Label
                :text="paymentType"
                class="text-right text-muted"
                :visibility="!isLoading ? 'visible' : 'collapse'"
              ></Label>
            </StackLayout>
            <ActivityIndicator
              row="0"
              col="1"
              :busy="isLoading"
              :visibility="isLoading ? 'visible' : 'collapse'"
            ></ActivityIndicator>
          </GridLayout>
          <StackLayout class="hr-light m-10"></StackLayout>
          <GridLayout rows="auto" columns="auto,*" @tap="showShipping()" class="list-group-item">
            <Label row="0" col="0" text="Shipping Method"></Label>
            <Label row="0" col="1" :text="shippingType" class="text-right text-muted"></Label>
          </GridLayout>
          <StackLayout class="hr-light m-10"></StackLayout>
          <GridLayout rows="auto" columns="auto,*" class="list-group-item">
            <Label row="0" col="0" text="Total"></Label>
            <Label row="0" col="1" :text="'$ ' + total" class="text-right"></Label>
          </GridLayout>
          <StackLayout class="hr-light m-10"></StackLayout>
          <Label :text="errorMessage" class="text-danger" textWrap="true"></Label>
          <Button text="Buy" :isEnabled="canBuy" class="btn btn-primary btn-active" @tap="buy()"></Button>
          <ActivityIndicator
            :busy="paymentInProgress"
            :visibility="paymentInProgress ? 'visible' : 'collapse'"
          ></ActivityIndicator>
          <Label :text="successMessage" class="text-primary" textWrap="true"></Label>
          <StackLayout class="hr-light m-10"></StackLayout>
          <Label text="Debug Info"></Label>
          <Label :text="debugInfo" class="body" textWrap="true"></Label>
        </StackLayout>
      </Page>
    </template>
    
    <script>
    //Change the import of 'stripe.service.ts' to the right path
    import { StripeService, Listener } from "~/services/stripe.service.ts";
    let stripeService = new StripeService();
    var paymentSession = null;
    export default {
      data() {
        return {
          stripeItem: {
            id: 0,
            name: "Something to buy",
            price: 1200
          },
          paymentInProgress: false,
          canBuy: true,
          isLoading: false,
          paymentType: "",
          paymentImage: "",
          shippingType: "",
          total: "",
          debugInfo: "",
          successMessage: "",
          errorMessage: ""
        };
      },
      methods: {
        onPageLoaded(args) {
          var comp = this;
          paymentSession = stripeService.createPaymentSession(
            args.object,
            comp.stripeItem.price,
            new Listener(comp)
          );
        },
        showPaymentMethods() {
          return stripeService.showPaymentMethods(paymentSession);
        },
        showShipping() {
          return stripeService.showShipping(paymentSession);
        },
        buy() {
          this.paymentInProgress = true;
          this.canBuy = false;
          return stripeService.requestPayment(paymentSession);
        }
      }
    };
    </script>
    

    And make sure you have TypeScript installed tns install typescript