1

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?

Andre Sharghi
  • 111
  • 1
  • 7
  • You can not simply construct `new Page()`, it should be the page your component is held. If you still have, please share the complete clean code including everything you have in service and post the error log. – Manoj Aug 31 '19 at 10:18
  • I have updated the post now with complete code @Manoj – Andre Sharghi Aug 31 '19 at 10:43
  • I tried "require('ui/frame').topmost().currentPage" instead of "new Page()". Still no error and no change in view @Manoj – Andre Sharghi Aug 31 '19 at 11:20
  • Use `loaded` event of `cartPage` and pass it as `args.object` where `args` should be parameter of loaded event callback. – Manoj Aug 31 '19 at 16:09
  • I did what you said, args.object had a prop `typeName="Page"` so I quess it was the right object but still nothing. My pageload event looks like this now: `onPageLoaded(args) { var comp = this; paymentSession = stripeService.createPaymentSession( args.object, comp.stripeItem.price, new Listener(comp) ); }` – Andre Sharghi Aug 31 '19 at 21:14
  • Please share the sample project where the issue can be reproduced. – Manoj Aug 31 '19 at 21:16
  • I'm not sure why but I removed the import of "nativescript-stripe/standard" from my component and now it's working. Thank you very much for your help @Manoj – Andre Sharghi Aug 31 '19 at 22:17

1 Answers1

0

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

Andre Sharghi
  • 111
  • 1
  • 7