@vince
I tried adding this
as first parameter.
Still got this error:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'findBy')
at e.triggerSmartAction (client-cf1950b09e515…0e0b1478.js:8:23735)
e.triggerSmartAction
await in e.triggerSmartAction
onSyncToPaymentService (anonymous)
Yes, that’s my understanding. If you have a better way, that’s great. Just don’t want to break anything. It’s been working as is for quite some time…
duh… Both are below.
template:
{{!-- v1.6.0: Add ability to fetch loanNumber without subscriptionToken record --}}
<div class="wrapper" id="smv_add_token" {{did-update this.onRecordsChange @records}}>
<div class="wrapper-list">
<div class="sub-tok-list-table">
<div class="sub-tok-list-heading">
<div class="sub-tok-list-heading-row">
<div class="column sub-tok-ids">Subscription ID/Token ID</div>
<div class="column loan-number">Loan number</div>
<div class="column pm-info">Payment method info<br>(Vendor/Type/Last 4)</div>
</div>
</div>
<div class="sub-tok-list-body">
{{#each @records as |record|}}
<div
class="sub-tok-list-row {{if (eq this.selectedRecord record) 'selected'}}"
{{on 'click' (fn this.onSelectRecord record)}}
>
<div class="column sub-tok-ids">
<span class="value">
{{record.forest-subscription.id}}
<br>
{{record.forest-token.id}}
<br>
{{#if record.forest-isPrimary}}
<div class="primary-token">primary</div>
{{/if}}
</span>
</div>
<div class="column loan-number">
<span class="value">{{record.forest-subscription.forest-loanNumber}}</span>
</div>
<div class="column pm-info">
<span class="value">{{record.forest-subscription.forest-vendor}}</span>
<span class="value"> / {{record.forest-token.forest-paymentMethod}} / </span>
<span class="value">{{record.forest-token.forest-last4}}</span>
</div>
</div>
{{/each}}
</div>
</div>
<DataDisplay::Table::TableFooter
@collection={{@collection}}
@viewList={{@viewList}}
@records={{@records}}
@currentPage={{@currentPage}}
@numberOfPages={{@numberOfPages}}
@recordsCount={{@recordsCount}}
@isLoading={{@isLoading}}
@fetchRecords={{@fetchRecords}}
/>
</div>
<div class="wrapper-forms">
{{#if this.stripe}}
<h2 class="forms-title">New payment method for Loan: <b>{{this.loanNumber}}</b></h2>
{{else}}
<h2 class="forms-title">Getting ready ...</h2>
{{/if}}
{{#if this.resultToken}}
{{#if (eq this.resultToken.paymentMethod 'CARD')}}
<div class="wrapper-result">
<div class="result-row">
<div class="label">Name</div>
<div class="value">{{this.resultToken.name}}</div>
</div>
<div class="result-row">
<div class="label">Card brand</div>
<div class="value">{{this.resultToken.type}}</div>
</div>
<div class="result-row">
<div class="label">Last 4 digits</div>
<div class="value">{{this.resultToken.last4}}</div>
</div>
<div class="result-row">
<div class="label">Expiry date</div>
<div class="value">{{this.resultToken.expiry}}</div>
</div>
{{#if this.resultToken.billingZip}}
<div class="result-row">
<div class="label">Billing zip</div>
<div class="value">{{this.resultToken.billingZip}}</div>
</div>
{{/if}}
</div>
{{else}}
<div class="wrapper-result">
<div class="result-row">
<div class="label">Bank name</div>
<div class="value">{{this.resultToken.bankName}}</div>
</div>
<div class="result-row">
<div class="label">Account owner's full name</div>
<div class="value">{{this.resultToken.name}}</div>
</div>
<div class="result-row">
<div class="label">Last 4 digits</div>
<div class="value">{{this.resultToken.last4}}</div>
</div>
{{#if this.resultToken.type}}
<div class="result-row">
<div class="label">Account type</div>
<div class="value">{{this.resultToken.type}}</div>
</div>
{{/if}}
</div>
{{/if}}
{{#if this.isClickedToSync}}
<h2 class="warning">Please refresh your browser to see new update after finishing the synchronization successfully.</h2>
{{else}}
<button class="btn btn-sync" {{on 'click' (fn this.onSyncToPaymentService)}}>
Upload new Payment method to Payment service
</button>
{{/if}}
{{else}}
<div data-loaded="{{this.isLoadedStripe}}"
data-visibility="{{if (eq this.vendor 'stripe') true false}}"
class="wrapper-stripe-forms">
{{!-- STRIPE CARD --}}
<form id="stripe-form" data-visibility="{{if this.resultToken false true}}"
onsubmit={{fn this.onSubmitToCreateStripeCard}}>
<div class="stripe-form-row">
<input id="stripe-card-holder-name" class="form-input" placeholder="Name" />
<div id="stripe-card-holder-name-error" class="stripe-error" role="alert" />
</div>
<div class="stripe-form-row">
<div id="stripe-card-element" />
<div id="stripe-card-errors" class="stripe-error" role="alert" />
</div>
<div class="form-actions">
{{#if this.isCreatingStripeCard}}
<div class="wrapper-dots">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
{{else}}
<button type="submit" class="btn btn-stripe-add">Add Card</button>
{{/if}}
</div>
</form>
{{!-- STRIPE ACH --}}
<form class="stripe-ach-form" onsubmit={{fn this.onSubmitToCreateStripeACH }}>
<div class="stripe-ach-form-cols-wrapper">
<div class="stripe-ach-form-col col-inputs">
<div class="ach-form-input">
<input type="text" id="stripe-ach-holder-name" placeholder="Account owner full name">
<div id="stripe-ach-holder-name-error" class="stripe-error" role="alert" />
</div>
<div class="ach-form-input">
<input type="text" id="stripe-ach-account-number" placeholder="Account number">
<div id="stripe-ach-account-number-error" class="stripe-error" role="alert" />
</div>
<div class="ach-form-input">
<input type="text" id="stripe-ach-routing-number" placeholder="Routing number">
<div id="stripe-ach-routing-number-error" class="stripe-error" role="alert" />
</div>
</div>
<div class="stripe-ach-form-col col-selectors">
<div class="ach-form-selector">
<label for="bank_country">Bank country</label>
<select id="stripe-ach-bank-country" name="bank_country" disabled>
<option value="US" selected>United States</option>
</select>
</div>
<div class="ach-form-selector">
<label for="currency">Currency</label>
<select id="stripe-ach-currency" name="currency" disabled>
<option value="USD" selected>USD</option>
</select>
</div>
<div class="ach-form-selector">
<label for="account_type">Account type</label>
<select id="stripe-ach-account-type" name="account_type">
<option value="individual">Individual</option>
<option value="company">Company</option>
</select>
</div>
</div>
</div>
<div class="form-actions">
{{#if this.isCreatingStripeACH}}
<div class="wrapper-dots">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
{{else}}
<button disabled={{this.isCreatingStripeACH}} type="submit" class="btn btn-stripe-add">Add ACH</button>
{{/if}}
</div>
</form>
</div>
{{#if (eq this.vendor 'payix')}}
<div class="wrapper-payix-iframes">
<div class="warning-pad">PAD only supports for Canadian</div>
<div class="wrapper-iframes">
<iframe src="{{this.payixIframeCardAchURL}}" />
<iframe src="{{this.payixIframePadURL}}" />
</div>
</div>
{{/if}}
{{/if}}
</div>
</div>
component (current testing state)
// v1.6.1: Update to new syntax
import Component from "@ember/component";
import { scheduleOnce } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { loadExternalJavascript, triggerSmartAction } from 'client/utils/smart-view-utils';
const CARD_STYLE = {
base: {
color: "#54BD7E",
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
fontSmoothing: "antialiased",
fontSize: "14px",
"::placeholder": {
color: "#aab7c4", // --color-beta-on-surface_light
},
},
invalid: {
color: "#FB6669",
iconColor: "#FB6669",
},
};
const ELEMENT_IDS = {
STRIPE_CARD_ELEMENT: "stripe-card-element",
STRIPE_CARD_ERROR: "stripe-card-errors",
STRIPE_CARD_HOLDER_NAME: "stripe-card-holder-name",
STRIPE_CARD_HOLDER_NAME_ERROR: "stripe-card-holder-name-error",
STRIPE_ACH_HOLDER_NAME: "stripe-ach-holder-name",
STRIPE_ACH_ACCOUNT_NUMBER: "stripe-ach-account-number",
STRIPE_ACH_ROUTING_NUMBER: "stripe-ach-routing-number",
STRIPE_ACH_ACCOUNT_TYPE: "stripe-ach-account-type",
STRIPE_ACH_HOLDER_NAME_ERROR: "stripe-ach-holder-name-error",
STRIPE_ACH_ACCOUNT_NUMBER_ERROR: "stripe-ach-account-number-error",
STRIPE_ACH_ROUTING_NUMBER_ERROR: "stripe-ach-routing-number-error",
};
const PAYMENT_METHOD = {
CARD: "CARD",
ACH: "ACH",
PAD: "PAD",
};
const PAYMENT_METHOD_VENDOR = {
STRIPE: "stripe",
PAYIX: "payix",
};
const STRIPE_ACH_DEFAULT_VALUE = {
BANK_COUNTRY_CODE: "US",
CURRENCY: "USD",
};
const obfuscateToken = (token) =>
token.includes("_")
? token.replace(/^(.*)_((.{3}).*(.{3}))$/, "$1_$3***$4")
: token.replace(/^(.{3})(.*)(.{3})$/, "$1***$3");
export default class extends Component {
@service() customAction;
@service() store;
@tracked selectedRecord = null;
@tracked resultToken = null;
@tracked stripe = null;
@tracked isLoadedStripe = false;
@tracked stripeCard = null;
@tracked isCreatingStripeCard = false;
@tracked isCreatingStripeACH = false;
@tracked isClickedToSync = false;
@tracked stripePublicKey = null;
@tracked payixIframeCardAchURL = null;
@tracked payixIframePadURL = null;
@tracked subscriptionId = null;
@tracked vendor = null;
@tracked loanNumber = null;
constructor(...args) {
super(...args);
console.log(`===== Smart view: v1.6.1: Update to new syntax =====`);
}
async didInsertElement() {
await this.setDefaultSelectedRecord();
this.onCreatedPayixToken = this.onCreatedPayixToken.bind(this);
window.addEventListener("message", this.onCreatedPayixToken);
}
willRemoveElement() {
window.removeEventListener("message", this.onCreatedPayixToken);
}
@action
onRecordsChange() {
this.setDefaultSelectedRecord();
}
isSupportedStripe() {
return this.vendor === PAYMENT_METHOD_VENDOR.STRIPE;
}
setURLs(data) {
this.payixIframeCardAchURL = data["forest-payix_iframe_card_ach_url"];
this.payixIframePadURL = data["forest-payix_iframe_pad_url"];
this.stripePublicKey = data["forest-stripe_public_key"];
}
setSubscriptionIdAndVendor(record) {
if (this.records) {
this.subscriptionId = record["forest-subscription"]["id"];
this.vendor = record["forest-subscription"]["forest-vendor"];
this.loanNumber = record["forest-subscription"]["forest-loanNumber"];
}
if (!this.records) {
this.subscriptionId = record["forest-id"];
this.vendor = record["forest-vendor"];
this.loanNumber = record["forest-loanNumber"];
}
}
setDefaultSelectedRecord() {
if (this.records && this.records.recordData) {
this.subscriptionId = this.records.recordData.id;
this.vendor = PAYMENT_METHOD_VENDOR.STRIPE;
if (this.records.recordData.__data) {
this.setURLs(this.records.recordData.__data);
//Setting the vendor here again as if it's a Canadian User
//The Vendor should be Payix not Stripe
if (this.records.recordData.__data["forest-vendor"]) {
this.vendor = this.records.recordData.__data["forest-vendor"];
}
}
if (!this.stripe) this.loadPlugins();
}
if (!this.selectedRecord) {
if (this.records && this.records.firstObject) {
this.selectedRecord = this.records.firstObject;
this.setURLs(this.records.firstObject);
this.setSubscriptionIdAndVendor(this.records.firstObject);
if (!this.stripe) this.loadPlugins();
}
if (!this.records) {
const regex = /sub_[\w-]+/;
const url = window.location.href
const hasSubscriptionId = url.match(regex)
if (hasSubscriptionId) {
const subscriptionId = url.match(regex)[0]
this.store.findRecord('forest-subscription', subscriptionId)
.then((record) => {
this.setURLs(record)
this.setSubscriptionIdAndVendor(record)
if (!this.stripe) this.loadPlugins();
})
}
}
}
}
loadPlugins() {
scheduleOnce("afterRender", this, async () => {
try {
const stripePublicKey = this.stripePublicKey;
await loadExternalJavascript('//js.stripe.com/v2/');
await loadExternalJavascript('//js.stripe.com/v3/');
Stripe.setPublishableKey(stripePublicKey); // v2
const stripe = Stripe(stripePublicKey); // v3
this.stripe = stripe;
this.isLoadedStripe = true;
if (this.isSupportedStripe()) {
this.renderStripeElements();
}
} catch (error) {
console.error("Internal Error: Failed to load Stripe:\n", error);
}
});
}
renderStripeCard() {
const { stripe } = this;
const elements = stripe.elements();
const stripeCard = elements.create("card", { style: CARD_STYLE });
stripeCard.mount(`#${ELEMENT_IDS.STRIPE_CARD_ELEMENT}`);
stripeCard.on("change", function (event) {
let errorElement = document.getElementById(ELEMENT_IDS.STRIPE_CARD_ERROR);
if (event.error) {
errorElement.textContent = event.error.message;
} else {
errorElement.textContent = "";
}
});
this.stripeCard = stripeCard;
}
renderStripeElements() {
if (!this.stripe) return;
this.renderStripeCard();
}
onCreatedStripeCardToken(cardData, cardName) {
try {
const {
id,
billing_details: {
address: { postal_code },
},
card: { brand, last4, exp_month, exp_year },
} = cardData;
const expiry = `${exp_year}-${("0" + exp_month).slice(-2)}-01`;
const resultToken = {
paymentMethod: PAYMENT_METHOD.CARD,
name: cardName,
token: id,
last4,
type: brand,
expiry,
billingZip: postal_code,
};
console.log("===== Created Stripe Card Token =====\n", {
...resultToken,
token: obfuscateToken(resultToken.token),
});
this.resultToken = resultToken;
} catch (error) {
console.error(
"Internal Error: Failed to add new Card with Stripe:\n",
error
);
}
}
onCreatedStripeACHToken(achData, achValues) {
try {
const {
id,
bank_account: { last4, bank_name },
} = achData;
const resultToken = {
paymentMethod: PAYMENT_METHOD.ACH,
token: id,
last4,
bankName: bank_name,
name: achValues.achHolderName,
type: achValues.achAccountType,
};
console.log("===== Created Stripe ACH Token =====\n", {
...resultToken,
token: obfuscateToken(resultToken.token),
});
this.resultToken = resultToken;
} catch (error) {
console.error(
"Internal Error: Failed to add new ACH with Stripe:\n",
error
);
}
}
onCreatedPayixToken(event) {
try {
const isAddedWithPayixSuccessfully =
event.data &&
event.data.token !== "" &&
event.data.token !== null &&
event.data.token !== undefined;
if (isAddedWithPayixSuccessfully) {
const {
token,
nameOnCard,
nameOnAch,
nameOnPad,
cardBrand,
expiryDate,
lastFourDigits,
billingZip,
bankName,
} = event.data;
const isCard = !!nameOnCard;
const isACH = !isCard && !!nameOnAch;
const isPAD = !isCard && !isACH && !!nameOnPad;
let resultToken;
if (isCard) {
const expiry = expiryDate.replace(/(\d{2})\/(\d{4})/, "$2-$1-01");
resultToken = {
paymentMethod: PAYMENT_METHOD.CARD,
name: nameOnCard,
token,
last4: lastFourDigits,
type: cardBrand,
expiry,
billingZip,
};
} else if (isACH) {
resultToken = {
paymentMethod: PAYMENT_METHOD.ACH,
name: nameOnAch,
token,
last4: lastFourDigits,
bankName,
};
} else if (isPAD) {
resultToken = {
paymentMethod: PAYMENT_METHOD.PAD,
name: nameOnPad,
token,
last4: lastFourDigits,
bankName,
};
}
if (resultToken) {
console.log(
`===== Created Payix ${resultToken.paymentMethod} Token =====\n`,
{
...resultToken,
token: obfuscateToken(resultToken.token),
}
);
this.resultToken = resultToken;
}
}
} catch (error) {
console.error(
"Internal Error: Failed to add new payment method with Payix:\n",
error
);
}
}
validateStripeACHForm() {
const { value: achHolderName } = document.getElementById(
ELEMENT_IDS.STRIPE_ACH_HOLDER_NAME
);
const { value: achAccountNumber } = document.getElementById(
ELEMENT_IDS.STRIPE_ACH_ACCOUNT_NUMBER
);
const { value: achRoutingNumber } = document.getElementById(
ELEMENT_IDS.STRIPE_ACH_ROUTING_NUMBER
);
const { value: achAccountType } = document.getElementById(
ELEMENT_IDS.STRIPE_ACH_ACCOUNT_TYPE
);
const holderNameErrorElement = document.getElementById(
ELEMENT_IDS.STRIPE_ACH_HOLDER_NAME_ERROR
);
const accountNumberErrorElement = document.getElementById(
ELEMENT_IDS.STRIPE_ACH_ACCOUNT_NUMBER_ERROR
);
const routingNumberErrorElement = document.getElementById(
ELEMENT_IDS.STRIPE_ACH_ROUTING_NUMBER_ERROR
);
let isValidHolderName = false;
if (!achHolderName) {
holderNameErrorElement.textContent =
"Please input Account ownner's fullname";
} else {
holderNameErrorElement.textContent = "";
isValidHolderName = true;
}
let isValidAccountNumber = false;
if (!achAccountNumber) {
accountNumberErrorElement.textContent = "Please input Account number";
} else {
const isValidAccountNumberByStripe = Stripe.bankAccount.validateAccountNumber(
achAccountNumber,
STRIPE_ACH_DEFAULT_VALUE.BANK_COUNTRY_CODE
);
if (!isValidAccountNumberByStripe) {
accountNumberErrorElement.textContent =
"Your Account number is invalid";
} else {
accountNumberErrorElement.textContent = "";
isValidAccountNumber = true;
}
}
let isValidRoutingNumber = false;
if (!achRoutingNumber) {
routingNumberErrorElement.textContent = "Please input Routing number";
} else {
const isValidRoutingNumberByStripe = Stripe.bankAccount.validateRoutingNumber(
achRoutingNumber,
STRIPE_ACH_DEFAULT_VALUE.BANK_COUNTRY_CODE
);
if (!isValidRoutingNumberByStripe) {
routingNumberErrorElement.textContent =
"Your Routing number is invalid";
} else {
routingNumberErrorElement.textContent = "";
isValidRoutingNumber = true;
}
}
const isValid = [
isValidHolderName,
isValidAccountNumber,
isValidRoutingNumber,
].every((isValidValue) => isValidValue);
if (!isValid) {
return null;
}
return {
achHolderName,
achAccountNumber,
achRoutingNumber,
achAccountType,
};
}
@action
onSelectRecord(record) {
this.selectedRecord = record;
this.resultToken = null;
this.setURLs(record);
this.setSubscriptionIdAndVendor(record);
if (this.isSupportedStripe()) {
this.renderStripeElements();
}
}
@action
onSyncToPaymentService() {
try {
const subscriptionId = this.subscriptionId;
if (!this.resultToken || !this.resultToken.token) {
throw new Error("Missing Token data");
}
const tokenData = {
...this.resultToken,
subscriptionId,
vendor: this.vendor,
};
console.log("===== Sync to Payment Service =====\n", {
...tokenData,
token: obfuscateToken(tokenData.token),
vendor: this.vendor,
});
this.isClickedToSync = true;
//Getting Add Token Custom Action
const customActions = this.store
.peekAll("custom-action")
.filter((c) => c.name === "Add Token");
console.log("===== CustomAction is retreived =====\n");
//Setting the customerActionValues with the tokenData
customActions[0].fields.forEach((f) => {
f.customActionValue = tokenData[f.fieldName];
});
console.log("===== CustomAction Fields are set =====\n");
if (!this.selectedRecord) {
this.selectedRecord = {};
}
//Triggering CustomAction
triggerSmartAction(
this,
customActions[0].name,
[this.selectedRecord],
tokenData
);
console.log("===== CustomAction is triggered =====\n");
} catch (error) {
console.error(
"Internal Error: Failed to sync to Payment Service:\n",
error
);
}
}
@action
async onSubmitToCreateStripeCard(event) {
event.preventDefault();
const { value: cardHolderName } = document.getElementById(
ELEMENT_IDS.STRIPE_CARD_HOLDER_NAME
);
let stripeCardNameErrorElement = document.getElementById(
ELEMENT_IDS.STRIPE_CARD_HOLDER_NAME_ERROR
);
if (!cardHolderName) {
stripeCardNameErrorElement.textContent =
"Please input card's owner name";
} else {
stripeCardNameErrorElement.textContent = "";
try {
this.isCreatingStripeCard = true;
const { value: cardName } = document.getElementById(
ELEMENT_IDS.STRIPE_CARD_HOLDER_NAME
);
const { stripe, stripeCard } = this;
const { paymentMethod: cardData } = await stripe.createPaymentMethod({
type: "card",
card: stripeCard,
billing_details: { name: cardName },
});
this.isCreatingStripeCard = false;
this.onCreatedStripeCardToken(cardData, cardName);
} catch (error) {
let stripeCardErrorElement = document.getElementById(
ELEMENT_IDS.STRIPE_CARD_ERROR
);
stripeCardErrorElement.textContent = error.message;
}
}
}
@action
onSubmitToCreateStripeACH(event) {
event.preventDefault();
const achValues = this.validateStripeACHForm();
if (!achValues) {
return;
}
try {
this.isCreatingStripeACH = true;
Stripe.bankAccount.createToken(
{
country: STRIPE_ACH_DEFAULT_VALUE.BANK_COUNTRY_CODE,
currency: STRIPE_ACH_DEFAULT_VALUE.CURRENCY,
account_holder_name: achValues.achHolderName,
account_number: achValues.achAccountNumber,
routing_number: achValues.achRoutingNumber,
account_holder_type: achValues.achAccountType,
},
(status, response) => {
if (response.error) {
throw error;
}
this.isCreatingStripeACH = false;
this.onCreatedStripeACHToken(response, achValues);
}
);
} catch (error) {
let stripeCardErrorElement = document.getElementById(
ELEMENT_IDS.STRIPE_CARD_ERROR
);
stripeCardErrorElement.textContent = error.message;
}
}
}