Skip to content

Instantly share code, notes, and snippets.

@tony-landis
Created May 22, 2023 17:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tony-landis/1f6635c0d42c43f2d96d069f0ac7bc91 to your computer and use it in GitHub Desktop.
Save tony-landis/1f6635c0d42c43f2d96d069f0ac7bc91 to your computer and use it in GitHub Desktop.
nmi pay page
import * as React from 'react';
import { NextPageContext } from 'next';
import Error from 'next/error'
import Head from 'next/head';
import compact from 'lodash/compact';
// import find from 'lodash/find';
// Material
import { Theme, withStyles, WithStyles, createStyles } from '@material-ui/core/styles';
import Avatar from '@material-ui/core/Avatar';
import Button from '@material-ui/core/Button';
import CircularProgress from '@material-ui/core/CircularProgress';
import Grid from '@material-ui/core/Grid';
import InputAdornment from '@material-ui/core/InputAdornment';
import Paper from '@material-ui/core/Paper';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
// Icons
import LockIcon from '@material-ui/icons/Lock';
// Anywhere
// Components
import CancelTable from 'anywhere/next/client/CancellationTable';
import CustomButtonBox from 'anywhere/next/client/CustomButtonBox';
import Accordion from 'anywhere/next/client/ControlledAccordion';
import HomeAppBar from 'anywhere/next/client/HomeAppBar';
import TermsOfService from 'anywhere/next/general-components/TermsOfService';
// Icons
import FireworksIcon from 'anywhere/next/svg/FireworksIcon';
import ExclamationCircleIcon from '@heroicons/react/20/solid/ExclamationCircleIcon';
// Libs
import formatDate from 'date-fn1/format';
import { currencyFormat } from 'anywhere/next/libs/text-manipulation';
import { analytics_identify } from 'anywhere/next/libs/siteAnalytics';
// API
import { PaymentRequest, CreditCardInfo, Postback, POST_pay_charge_with_token_by_id } from "anywhere/api/gen-api/private/POST_pay_charge_with_token_by_id";
import { POST_pay_charge_paypal_balance_by_id } from 'anywhere/api/gen-api/private/POST_pay_charge_paypal_balance_by_id';
import { POST_pay_charge_paypal_amount_by_id } from 'anywhere/api/gen-api/private/POST_pay_charge_paypal_amount_by_id';
import { R_itinerary_statement, GET_itinerary_statement_by_id } from 'anywhere/api/gen-api/private/GET_itinerary_statement_by_id';
import { R_itinerary_printable_v2, GET_itinerary_printable_wp_v2_by_id } from 'anywhere/api/gen-api/private/GET_itinerary_printable_wp_v2_by_id';
import { ItineraryRevReq, POST_itinerary_rev_by_id } from 'anywhere/api/gen-api/private/POST_itinerary_rev_by_id';
import { GET_itinerary_transaction_analytics_by_id } from 'anywhere/api/gen-api/private/GET_itinerary_transaction_analytics_by_id';
import { POST_itinerary_transaction_analytics_by_id } from 'anywhere/api/gen-api/private/POST_itinerary_transaction_analytics_by_id';
const fieldStyle = (theme:Theme)=>createStyles({
root:{
'& label.Mui-focused': {
color: theme.palette.secondary.dark,
},
'& label':{
[theme.breakpoints.only("xs")]:{
fontSize: theme.typography.body2.fontSize,
},
},
'& .MuiInput-underline:after': {
borderBottomColor: theme.palette.secondary.dark,
},
'& .MuiOutlinedInput-root': {
'&.Mui-focused fieldset': {
borderColor: theme.palette.secondary.dark,
},
},
}
});
const CssTextField = withStyles(fieldStyle)(TextField)
const CollectContext = React.createContext();
function reducer(state, action) {
switch (action.type) {
case 'addError':
return {
errors: state.errors.concat([{
fieldName: action.fieldName,
message: action.message
}])
};
case 'unsetErrors':
return {
errors: state.errors.filter(error => error.fieldName !== action.fieldName)
};
default:
throw new Error();
}
}
function CollectJSProvider({children, collectJSPromise}) {
const [state, dispatch] = React.useReducer(reducer, { errors: [] });
const addError = React.useCallback((fieldName, message) => {
dispatch({
type: 'addError',
fieldName,
message
})
}, [dispatch])
const unsetErrors = React.useCallback((fieldName) => {
dispatch({
type: 'unsetErrors',
fieldName
})
}, [dispatch])
return (
<CollectContext.Provider value={{
state,
collectJSPromise,
addError,
unsetErrors
}}>
{children}
</CollectContext.Provider>
)
}
function injectCollectJS(collectJsUrl, tokenizationKey) {
const script = document.createElement('script');
script.setAttribute('src', collectJsUrl);
script.setAttribute('data-tokenization-key', tokenizationKey);
script.setAttribute('data-variant', 'inline');
document.querySelector('body').appendChild(script)
return new Promise((resolve, reject) => {
script.onload = function() {
resolve(window.CollectJS);
}
})
}
const TokenizeToCustomer = ({setLoading, setError, onSuccessfulTokenization, withToken, children, amount }) => {
const collectJSConfig = {
customCss: {
"color": "#333",
"background-color": "#fff",
"border": "1px solid #ccc",
"border-radius": "3px",
"width": "100%",
},
placeholderCss: {
"color": "#666",
},
validCss: {
// "color": "green",
// "background-color": "#f1f1f1",
"border": "1px solid #ccc"
},
invalidCss: {
"color": "red",
"background-color": "#fff",
"border": "1px solid red"
},
focusCss: {
"color": "#333",
"border": "2px solid #5183ad",
},
// fields: {
// cardNumber: {
// selector: "#ccnumber",
// },
// cardExpiry: {
// selector: "#ccexp",
// },
// }
fields: {
"googlePay": {
"selector": ".googlepaybutton",
"shippingAddressRequired": false,
// "shippingAddressParameters": {
// "phoneNumberRequired": false,
// "allowedCountryCodes": ['US', 'CA', 'UK']
// },
"billingAddressRequired": false,
"billingAddressParameters": {
"phoneNumberRequired": false,
"format": "MIN"
},
'emailRequired': false,
'buttonType': 'book',
// 'buttonColor': 'white',
'buttonLocale': 'en'
},
'applePay' : {
// 'selector' : '.applepaybutton',
'shippingType': 'delivery',
'requiredBillingContactFields': [
'postalAddress',
'name'
],
'lineItems': [
{
'label': 'Total',
'amount': amount.toString(),
}
],
'type': 'book',
// 'style': {
// 'button-style': 'white-outline',
// 'height': '50px',
// 'border-radius': '0'
// }
},
},
"price": amount.toString(),
"currency": "USD",
"country": "US", // TODO will this break if user is not in USA?
"timeoutDuration": 10000,
"timeoutCallback": function () {
console.log("The tokenization didn't respond in the expected timeframe. This could be due to an invalid or incomplete field or poor connectivity");
setError('Please check the form and try again - all fields are required.')
setLoading && setLoading(false);
},
"fieldsAvailableCallback": function () {
console.log("Collect.js loaded the fields onto the form");
},
callback: function(e) {
console.log('callback: ', e)
// setPaymentToken(e);
onSuccessfulTokenization && onSuccessfulTokenization(e);
},
validationCallback: function(fieldName, valid, message) {
console.log('validationCallback', fieldName, valid, message)
// if (valid) {
// unsetErrors(fieldName);
// } else {
// unsetErrors(fieldName);
// addError(fieldName, message);
// }
},
// 'callback': function (response) {
// console.log('callback() - token: ', response.token);
// console.log(response)
// // var input = document.createElement("input");
// // input.type = "hidden";
// // input.name = "payment_token";
// // input.value = response.token;
// // var form = document.getElementsByTagName("form")[0];
// // form.appendChild(input);
// // form.submit();
// }
}
const [paymentToken, setPaymentToken] = React.useState(null);
const [collect, setCollect] = React.useState(null);
const { unsetErrors, addError, collectJSPromise } = React.useContext(CollectContext);
const reset = React.useCallback(() => {
console.log('useCollect reset')
collect?.retokenize();
setPaymentToken(null);
}, [collect]);
React.useEffect(() => {
console.log('paymentToken', paymentToken)
}, [paymentToken])
React.useEffect(() => {
collectJSPromise.then((collectJS) => {
setCollect(collectJS);
collectJS.configure({
...collectJSConfig,
})
})
// No dependencies - we don't ever want this to run more than once. Calling this more times will cause fields to blink.
}, [])
const onSubmit = React.useCallback((e) => {
e.preventDefault();
console.log('submitting')
console.log(collect)
collect.startPaymentRequest();
}, [collect]);
const [tab, setTab] = React.useState('credit');
const onRestart = React.useCallback((e) => {
e.preventDefault();
reset();
}, [reset]);
return children({onSubmit, onRestart, collect, paymentToken, reset})
}
const _localize = (en, _es) => en
// get the users ip address, check we are in the browser
const getIpAddress = () : Promise<string | null | undefined> => {
return fetch('https://www.cloudflare.com/cdn-cgi/trace')
.catch(err => {
console.log(err)
return null
})
.then(res => res?.text())
.then(data => {
let ipRegex = /[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}/
let ip = data?.match(ipRegex)[0];
// console.log(ip);
return ip || ''
});
}
export type Props ={
userId: number | null,
itineraryId: number,
userEmail: string | null,
planFkey : string | null,
showSavedCards : boolean,
minPay?: boolean,
payWall: number | null,
customAmount?: number | null,
reserveIds?: number[] | null,
customText?: string,
startStep?: string | null,
alert?: boolean | null,
};
export type State ={
step: number,
paymentMethod : "cc" | "paypal" | "ach",
paymentAmount : number,
paymentType : "deposit" | "balance" | "other",
paymentRequest : PaymentRequest,
ccInfo : CreditCardInfo,
selectCC : string,
disableNext : boolean,
expDate : string,
ccErrors: { ccNum?: boolean, expDate?: boolean, ccName?: boolean, ccZip?: boolean, ccSecCode?: boolean, billingAdd?: boolean },
processing: boolean,
errorMsg: string | JSX.Element,
postbackId: number,
paymentScheduleInstructions: string,
customTip: boolean,
customCarbon: boolean,
tipPostback: number,
newStatement: R_itinerary_statement | null;
paypalUrl: string,
reserves: Array<R_itinerary_printable_v2> | null;
statement : R_itinerary_statement;
errorCode ?: number,
showTOS: boolean,
termsAck: boolean,
};
class PaymentForm extends React.Component<Props & WithStyles<typeof useStyles>,State>{
// static contextType = AppContext;
static async getInitialProps ({res, req, query} : NextPageContext) {
const { slug, email, mp, paywall,ca, rid, ct, step, alert } = query;
// const locale = 'en';
const itineraryId = slug.length > 1 ? slug[1] : slug[0]
const planFkey = slug.length > 1 ? slug[0] : null
const itId = itineraryId ? parseInt(itineraryId, 10) : 0;
const reserves = rid ? rid+'' : null;
const reserveIdArray = reserves && reserves.split(',').map(id => isNaN( parseInt(id) ) ? 0 : parseInt(id)).filter( id => id > 0) || []
return {
itineraryId: itId,
planFkey: planFkey || null,
userEmail: email || null,
minPay: mp && mp > 0,
payWall: paywall && parseFloat(paywall) || null,
customAmount: ca && parseFloat(ca) || null,
customText: ct && ct + '' || null,
reserveIds: reserveIdArray,
startStep: step || null,
alert: alert && parseInt(alert) > 0
}
}
componentDidMount(){
const { itineraryId, planFkey, userEmail, reserveIds} = this.props;
const itinPrint = GET_itinerary_printable_wp_v2_by_id(itineraryId,{locale: 'EN', plan_fkey: planFkey, email: userEmail || null});
const itStatement = GET_itinerary_statement_by_id(itineraryId, { plan_fkey: planFkey, email: userEmail || null});
const collectJS = injectCollectJS(
'https://secure.networkmerchants.com/token/Collect.js',
// '38c46b-82hRF9-44uZaC-vjTtqy' staging
'RAYKw9-9erAJn-YUr39R-M9Y3Ar'
);
this.setState({collectJS: collectJS})
return Promise.all([itinPrint, itStatement, getIpAddress()])
.then( (res: [R_itinerary_printable_v2[], R_itinerary_statement, string | null]) => {
const reserves = reserveIds?.length > 0
? res[0].filter( service => reserveIds?.indexOf(service.reserve_id) > -1 )
: res[0];
this.setState({
statement: res[1],
reserves: reserves,
errorCode: undefined,
paymentRequest: {...this.state.paymentRequest, ipAddress: res[2]}
})
})
.catch( err => {
const errorCode = err?.cause?.status;
console.log(`WARN errorCode: ${errorCode} on account-payment - GET_itinerary_statement_by_id`)
this.setState ({ errorCode : errorCode });
})
}
resetValue : State = {
step : this.props.startStep ? 1 : 0,
paymentMethod : "cc",
paymentAmount : this.props.payWall
|| this.props.customAmount
|| this.props.statement && this.props.statement.balance_due_usd
|| 0,
paymentType : null, // (this.props.payWall || this.props.customAmount)
// ? "other"
// : "balance",
paymentRequest : new PaymentRequest({ cardId: (this.props.statement && this.props.statement.billable_cards) ? undefined : 1, reserveIds: this.props.reserveIds && this.props.reserveIds.length > 0 && this.props.reserveIds.map(num => num.toString()) || null }),
ccInfo : new CreditCardInfo({expMonth: new Date().getMonth() + 1, expYear: new Date().getFullYear(),token:"",}),
selectCC : (this.props.statement && this.props.statement.billable_cards) ? "" : "new" ,
disableNext : false,
expDate: "",
ccErrors: { ccNum: undefined, expDate: false, ccName: undefined, ccZip: undefined, ccSecCode: undefined, billingAdd: undefined },
processing: false,
errorMsg : "",
postbackId: 0,
paymentScheduleInstructions: "",
customTip: false,
customCarbon: false,
tipPostback: 0,
newStatement: null,
paypalUrl: '',
showTOS: false,
termsAck: false,
}
state : State = this.resetValue;
customPaymentRef = React.createRef();
paypalWait = (paypalWindow:Window | null) => {
const {itineraryId, planFkey, userEmail } = this.props;
const waitToPay = setInterval( ()=> {
if( paypalWindow && paypalWindow.closed ){
clearInterval(waitToPay);
console.log(`Paypal Window closed`)
setTimeout( () => {
GET_itinerary_statement_by_id( itineraryId, { plan_fkey: planFkey, email: userEmail || null })
.then( res => {
if( res.postback_last && res.postback_last.payment_method == "paypal" && res.postback_last.status == 1 ){
const latestPayment = res.payments && res.payments.length > 0 && res.payments.pop() || 'Ref # PB0000';
const idIndex = latestPayment.indexOf('Ref # PB');
this.setState({ newStatement: res, step: 3, disableNext: false, postbackId: parseInt( latestPayment.slice(idIndex + 8) , 10) || 0 });
return;
}
this.setState({errorMsg: this.context._localize(`We can't find your payment, please contact our team, try again or try using a credit card.`,'No encontramos tu pago, por favor contacta a nuestro equipo, prueba de nuevo o intenta con tarjeta de crédito.')})
})
.catch( err => {
console.log(`WARN - error code ${err.cause.status} from EP - GET_itinerary_statement_by_id after PaypalValidation`)
this.setState({errorMsg: this.context._localize(`Something went wrong, please contact our team.`,'Algo salió mal, por favor contacta a nuestro equipo.')})
})
}
, 2000)
}
else console.log('waiting')
}, 1000)
}
handleSendNotes = () => {
if(this.state.paymentScheduleInstructions.length > 7){
const revReq : ItineraryRevReq = new ItineraryRevReq({
itineraryRevReqNotes: '[Payment Schedule] \n' + this.state.paymentScheduleInstructions,
itineraryRevReqFkey: this.props.planFkey || undefined,
itineraryRevReqName: '',
})
POST_itinerary_rev_by_id(this.props.itineraryId, revReq)
.then( res => {
console.log('Payment Schedule Notified');
})
.catch( err => {
console.log(`WARN - Error code adding schedule notes in account-payment: ${err.cause.status} on EP POST_itinerary_rev_by_id on I${this.props.itineraryId}`)
})
}
};
paypalOpen = (paypalUrl:string) => {
// const paypalWindow = window.open(paypalUrl, "_blank");
window.open(paypalUrl, '_blank', 'width=500, height=600, resizable=yes, scrollbars=yes');
this.paypalWait(paypalWindow);
}
handleSelectPaymentMethod = ( value : State['paymentMethod'] ) => {
const defaultValues: State = {
...this.state,
paymentMethod: value,
paymentType: this.props.customAmount ? "other" : "balance",
paymentAmount: this.resetValue.paymentAmount,
disableNext: false,
errorMsg: '',
processing: false,
};
if(value === "paypal" && this.state.paypalUrl.length == 0 && !this.props.customAmount){
POST_pay_charge_paypal_balance_by_id(this.props.itineraryId, new PaymentRequest({...this.state.paymentRequest, securityCode: "000"}))
.then( res => {
this.setState({ ...defaultValues, paypalUrl: res, })
this.paypalOpen(res)
})
.catch( err => {
console.log(`WARN - Error ${err.cause.status} on POST_pay_charge_paypal_balance_by_id`)
this.setState({errorMsg: 'Something went wrong, try with a credit card instead.', paypalUrl: ''})
})
}
if(value === "paypal" && this.state.paypalUrl.length == 0 && this.props.customAmount){
POST_pay_charge_paypal_amount_by_id(this.props.itineraryId, {amount: this.props.customAmount} ,new PaymentRequest({...this.state.paymentRequest, securityCode: "000"}))
.then( res => {
this.setState({ ...defaultValues, paypalUrl: res, })
this.paypalOpen(res)
})
.catch( err => {
console.log(`WARN - Error ${err.cause.status} on POST_pay_charge_paypal_amount_by_id`)
this.setState({errorMsg: 'Something went wrong, try with a credit card instead.', paypalUrl: ''})
})
}
else this.setState({ ...defaultValues });
};
handleSelectPaymentType = ( value : State['paymentType'], paymentAmount: number) => {
this.setState({
paymentType: value,
paymentAmount: paymentAmount,
disableNext: false,
},
() => (this.state.paymentType == "other") && this.customPaymentRef.current.focus()
)
};
render() {
const { itineraryId, minPay, payWall, customAmount, customText } = this.props;
const { paymentMethod, paymentType, paymentRequest, paymentAmount, ccInfo,
disableNext, ccErrors, processing, errorMsg, paymentScheduleInstructions,
customTip, tipPostback, postbackId, paypalUrl,
statement, reserves, errorCode, showTOS, termsAck } = this.state;
if((!statement || !reserves) && errorCode)
return <Error statusCode={errorCode}/>
if((!statement || !reserves) && !errorCode)
return <div style={{display:'flex', justifyContent: 'center', alignItems: 'center', marginTop: 40}}>
<br />
<br />
<br />
<CircularProgress thickness={5.5} size={60}/>
</div>;
const allowCustomPayment =( statement.itinerary.tags || []).indexOf('AllowCustomPayment') > -1
const hideOptions = statement.balance_due_usd && statement.balance_due_usd <= 50 || false;
const pastBillDate = statement.days_until_bill_date && statement.days_until_bill_date > 0 || false;
// const paidDeposit = statement.itinerary.deposit_status && statement.itinerary.deposit_status < 1 || false;
const paymentsMoreThanDeposit = statement.payments_usd && statement.itinerary.deposit_amount && ( -1 * statement.payments_usd) < statement.itinerary.deposit_amount || false;
const itDeposit = statement.itinerary.deposit_amount || 0 ;
const showDeposit = pastBillDate || paymentsMoreThanDeposit || false
const balanceDue = statement.balance_due_usd || 0;
const minPayment = showDeposit ? Math.max( 50, itDeposit ) : Math.min( balanceDue , ( paymentsMoreThanDeposit || !minPay ) ? 50 : itDeposit );
const tips = paymentRequest.tipAmount || 0;
const penaltyDays = reserves && compact( reserves.map( reserve => reserve.penalty_free_cancel_days || 0 )) || [0];
const minDays = reserves && Math.min( ...penaltyDays) > -150 && Math.min( ...penaltyDays) || 0;
const maxDays = reserves && Math.max( ...penaltyDays) < 150 && Math.max( ...penaltyDays) || 0;
const validateAmount = ( amount: number ) => {
const notValid = amount < minPayment;
this.setState({
paymentAmount: amount,
disableNext: notValid,
})
};
const itemList = () =>{
const fullItems = [
{
summary: <Typography>{_localize('Cancellation Policy','Políticas de Cancelación')}</Typography>,
details: <Grid container style={{padding: 16, paddingTop: 0}}>
<Grid item xs={12} >
{(minDays && maxDays) && <>
<Typography variant="body2" paragraph >
{( minDays !== maxDays ) && _localize(
`For your itinerary, dates for cancellation without penalties range from ${minDays} to ${maxDays} days in advance.`
, `Para tu itinerario, las fechas cancelación gratuita de servicios deberán ser notificados con ${minDays} a ${maxDays} días de anticipación respectivamente.`
)}
{( minDays == maxDays ) && _localize(
`For your itinerary, all services can be cancelled without penalties up ${minDays} days in advance.`
, `Para tu itinerario, todos los servicios pueden ser cancelados sin penalidad notificando con ${minDays} días de anticipación.`
)}
</Typography>
</> || null}
<Typography variant="body2" paragraph>
By selecting to complete this booking and pay, I acknowledge that I have read and accept the&nbsp;
<span onClick={() => this.setState({showTOS: !showTOS})} className="decorate-brand-link">
Terms and Conditions
</span>
&nbsp;and&nbsp;
<a
href="/company/privacy"
target="_blank"
className="decorate-brand-link"
>
Privacy Policy
</a>
</Typography>
</Grid>
{showTOS && <Grid item xs={12} >
<Paper>
<TermsOfService name={ccInfo.cardholder || statement.user.name || undefined} />
</Paper>
</Grid>}
<Grid item xs={12}>
{(reserves) && <CancelTable reserves={reserves} />}
</Grid>
</Grid>
},
{
summary: <Typography>{_localize("Review Bookings","Servicios Incluídos")}</Typography>,
details: <Grid container justifyContent="center" spacing={2} style={{padding: 16, paddingTop: 0}}>
{(reserves) && <Grid item xs={12}>
<Table size="small" style={{width: '100%', overflow: "hidden", textOverflow: "ellipsis"}}>
<TableHead>
<TableRow>
<TableCell align="justify">{_localize("Bookings","Reservas")}</TableCell>
<TableCell align="center">{_localize("Date","Fechas")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{reserves.map( (reserve, index) => {
return (<TableRow key={`reserveIndex${index}`}>
<TableCell align="justify" >
<Typography variant="caption" component="p">{reserve.reserve_type == "hotel" ? reserve.operator_name : reserve.reserve.service}</Typography>
{reserve.start_ts && <Typography variant="caption" component="p" >
<strong>{_localize("Date: ", "Fecha: ")}</strong>{formatDate(reserve.reserve?.start_date, 'D-MMM-\'YY')}
</Typography>}
</TableCell>
{reserve.start_ts && <TableCell align="center">
<Typography variant="caption" component="p">
{formatDate(reserve.reserve?.start_date, 'D-MMM-\'YY')}
</Typography>
</TableCell>}
</TableRow>)
} )}
</TableBody>
</Table></Grid>}
<Grid item>
<Typography component="a"
href={`/account/itinerary/printable/${itineraryId}?email=${statement.user.username}&mode=preview`}
target="_blank"
align="center"
variant="caption"
>
{_localize('Read your Confirmations Preview', 'Vista previa de tus confirmaciones')}
</Typography>
</Grid>
</Grid>
},
{
summary: <Typography>Payment Schedule</Typography>,
details: <Grid container spacing={2}>
<Grid item xs={12}>
<Typography variant="body2" component="p" className="px-3">
{_localize(`Tell us if you prefer to make a custom payment plan instead of paying the remaining balance on ${formatDate(statement.itinerary.bill_date, 'Do of MMMM YYYY')}.`
,`Dinos si prefieres un plan de pago a la medida en lugar de pagar el saldo en ${formatDate(statement.itinerary.bill_date, 'Do de MMMM YYYY')}.`)}
</Typography>
</Grid>
<Grid item xs={12}>
<div className="p-3">
<CssTextField
variant="outlined"
placeholder={_localize('Bill me $500 on the 1st of each month', 'Cobrenme $500 en el primero de cada mes')}
value={paymentScheduleInstructions}
onChange={ (e:React.ChangeEvent<HTMLInputElement>) => this.setState({paymentScheduleInstructions: e.target.value})}
multiline fullWidth
rows="4"
/>
</div>
</Grid>
</Grid>
},
];
if(statement.itinerary.bill_date
&& paymentType !== "balance"
&& statement.balance_due_usd
&& statement.balance_due_usd - paymentAmount > 0 && !customText )
return fullItems;
else
return fullItems.slice(0,2);
};
// Thanks
const thankYou = () => {
// TODO
// analyticsEvent(itineraryId); // Fire analytics
return <Grid container justifyContent="center" spacing={1}>
<Grid item style={{ marginTop: 25 }}>
<FireworksIcon style={{fontSize: 60, marginLeft: 'auto', marginRight: 'auto'}}/>
</Grid>
<Grid item xs={12}>
<Typography variant="h4" align="center" component="div" paragraph>
{_localize('Thank You!', '¡Muchas Gracias!')}
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="body1" align="center">
{_localize(
`You have paid ${currencyFormat(totalPaymentAmount, '$', ' USD')} and a receipt should be in your inbox. Reference #`
,`Has pagado ${currencyFormat(totalPaymentAmount, '$', ' USD')} y hemos enviado tu factura por correo electrónico. Número de Referencia #`)} <strong>J{postbackId}</strong>.
</Typography>
{/* {tipPostback >= 2 &&
<Typography align="center" style={{whiteSpace: "pre-line"}}>
You've just paid {tips > 0 && <>{_localize('a tip of', 'una propina de')} {currencyFormat(tips)}</>}{(tips > 0 && carbonOffset > 0) && _localize(' and ',' y ')}{carbonOffset > 0 && <>{currencyFormat(carbonOffset)} {_localize('for tree planting.\n', 'para plantar árboles.\n')}</>}
{_localize('Reference # ' ,'Referencia # ')}<strong>J{tipPostback}</strong>
</Typography>
} */}
</Grid>
</Grid>
}
const tipAmount = paymentRequest.tipAmount || 0;
const totalPaymentAmount = tipAmount + paymentAmount;
// TODO - these can be indidvidual components or in the render
const tipOptions = () => {
const totalAmount = statement.itinerary.total_amount && (statement.itinerary.total_amount > 100 ? statement.itinerary.total_amount : 100) || 100;
const tipAmount1 = totalAmount > 100 ? Math.round(totalAmount*0.01) : 1;
const tipAmount2 = totalAmount > 100 ? Math.round(totalAmount*0.02) : 3;
const tipAmount3 = totalAmount > 100 ? Math.round(totalAmount*0.03) : 5;
const staffFirst = statement.staff_name && (statement.staff_name.split(' '))[0] || '';
return <Grid container justifyContent="center" spacing={1}>
{(tipPostback < 1) && <>
{statement.staff_name && <>
<Grid item xs={12}>
{statement.staff_photo_src &&
<Avatar src={statement.staff_photo_src}
alt={statement.staff_name}
style={{
marginLeft: 'auto', marginRight: 'auto', height: 65, width: 65, marginBottom: '5px'
}} />
}
<Typography variant="body1" align="center">
{_localize(
`Help ${staffFirst.length > 0 ? staffFirst + ' &' : ''} team Anywhere keep providing outstanding service with a tip.`
,`Ayuda a ${staffFirst.length > 0 ? staffFirst + ' &' : ''} el equipo Anywhere a seguir proveyendo un servicio estelar con una propina.`)}
</Typography>
{( statement?.tips_amt > 0 ) &&
<Typography variant="caption" component="div" align="center">
{ currencyFormat(statement?.tips_amt) + _localize(' tipped already', 'en propinas dadas')}
</Typography> || null}
</Grid>
<CustomButtonBox userIsTouching={false}
xs={4} height='98px'
text={_localize('Good')}
subText={`${totalAmount > 100 ? '(1%)' : '' }\n${currencyFormat(tipAmount1)}`}
value={tipAmount1}
active={paymentRequest.tipAmount == tipAmount1}
onClick={value=>{
this.setState({ customTip: false, paymentRequest: new PaymentRequest({...paymentRequest, tipAmount: tips == value ? 0 : value}) })
}}
/>
<CustomButtonBox userIsTouching={false}
xs={4} height='98px'
text={_localize('Excellent')}
subText={`${totalAmount > 100 ? '(2%)' : '' }\n${currencyFormat(tipAmount2)}`}
value={tipAmount2}
active={tips == tipAmount2}
onClick={value=>{
this.setState({customTip: false, paymentRequest: new PaymentRequest({...paymentRequest, tipAmount: tips == value ? 0 : value}) })
}}
/>
<CustomButtonBox userIsTouching={false}
xs={4} height='98px'
text={_localize('Amazing')}
subText={`${totalAmount > 100 ? '(3%)' : '' }\n${currencyFormat(tipAmount3)}`}
value={tipAmount3}
active={tips == tipAmount3}
onClick={value=>{
this.setState({ customTip: false, paymentRequest: new PaymentRequest({...paymentRequest, tipAmount: tips == value ? 0 : value}) })
}}
/>
<Grid item>
{!customTip && <Button color="secondary" style={{textTransform: 'lowercase'}} onClick={()=>{
this.setState({customTip: true}, () => {
this.customPaymentRef.current.focus()
}) }}>
other amount
</Button>}
{tipAmount > 0 && <Button color="secondary" style={{textTransform: 'lowercase'}}
onClick={()=>{
this.setState({customTip: false, paymentRequest: new PaymentRequest({...paymentRequest, tipAmount: 0}) })
}}>
no tip
</Button>}
{customTip && <CssTextField
label={_localize("Custom tip","Propina")}
value={ tips.toString().replace(/^0+/, '') || 0}
type="number"
variant="outlined"
onChange={ (e:React.ChangeEvent<HTMLInputElement>) => this.setState({ paymentRequest: new PaymentRequest({ ...paymentRequest, tipAmount: Math.abs(parseFloat( e.target.value )) || 0 })}) }
InputProps={{ startAdornment: <InputAdornment position="start">USD</InputAdornment>}}
inputProps={{ min: 0 }}
inputRef={this.customPaymentRef}
/>}
</Grid>
</>}
</>}
</Grid>
}
const confirmTripDetails =
<Grid container>
<Grid item xs={12}>
<Accordion items={itemList()} />
</Grid>
</Grid>
const howMuchWillYouPay = <>
{/* <pre>
hideOptions: {JSON.stringify(!hideOptions)} <br/>
pastBillDate: {JSON.stringify(pastBillDate)} <br/>
payWall: {JSON.stringify(!payWall)} <br/>
customText: {JSON.stringify(!customText)} <br/>
allowCustomPayment: {JSON.stringify(allowCustomPayment)} <br/>
</pre> */}
{/* {((!hideOptions && pastBillDate && !payWall && !customText)
|| allowCustomPayment) && */}
{ true &&
<>
<Typography variant="h5" style={{marginTop: 10}} paragraph align="center">
{!payWall && !customText && ((!hideOptions && pastBillDate) ?
_localize('How much will you pay?','¿Cuánto pagarás?')
: _localize(`Remaining Balance: `, `Saldo Pendiente: `)+ currencyFormat(balanceDue, 'USD $'))}
{payWall && _localize(`Keep planning for `, `Sigue planeando por `) + currencyFormat(payWall,'USD $')}
{(customText && customAmount) && `${customText}: ${currencyFormat(customAmount, 'USD $')}`}
</Typography>
<Grid container justifyContent="center">
{ showDeposit && <CustomButtonBox
userIsTouching={false}
xs={4}
height='98px'
text={_localize('Minimum', 'Mínimo')}
subText={currencyFormat(itDeposit, 'USD $')}
value={"deposit"}
active={paymentType === "deposit"}
disabled={paymentMethod == "paypal"}
onClick={ value => this.handleSelectPaymentType(value, itDeposit)}
/>}
<CustomButtonBox
userIsTouching={false}
md={showDeposit ? 4 : 5 }
xs={ showDeposit ? 4 : 6}
height='98px'
text={_localize('Balance', 'Totalidad')}
subText={currencyFormat(balanceDue, 'USD $')}
value={"balance"}
active={paymentType === "balance"}
onClick={ value => this.handleSelectPaymentType(value, balanceDue)}
/>
<CustomButtonBox
userIsTouching={false}
md={showDeposit ? 4 : 5 }
xs={ showDeposit ? 4 : 6}
height='98px'
text={_localize('Other', 'Otro')}
subText={`min. ${currencyFormat(minPayment, 'USD $')}`}
value={"other"}
active={paymentType === "other"}
disabled={paymentMethod == "paypal"}
onClick={ value => this.handleSelectPaymentType(value, minPayment)}
/>
<Grid item xs={12}> <br/> </Grid>
{paymentType == "other" &&
<Grid item>
<CssTextField
label={_localize("Other amount","Otro monto")}
value={paymentAmount.toString().replace(/^0+/, '')}
type="number"
variant="outlined"
onChange={ (e:React.ChangeEvent<HTMLInputElement>) => validateAmount( parseFloat( e.target.value ) || 0 )}
InputProps={{ startAdornment: <InputAdornment position="start">USD</InputAdornment>}}
inputProps={{ min: minPayment }}
error={disableNext}
helperText={_localize('Minimum of $','Mínimo de $') + minPayment}
inputRef={this.customPaymentRef}
/>
<br/>
<br/>
</Grid>}
</Grid>
</>}
</>
const howWillYouPay = <>
{/* <Typography variant="h5" style={{marginTop: 10}} paragraph align="center">
{_localize('How will you pay?','¿Qué forma de pago usarás?')}
</Typography> */}
<Grid container justifyContent="center">
<CustomButtonBox
userIsTouching={false}
sm={6} md={5}
height='92px'
// topIcon={<img src={"https://www.anywhere.com/img-b/icons/icon-credit.png"} width={40} height={30}/>}
topIcon={<img src={"/img/icon-visa-mastercard-amex-american-express-clipart.png"} width={150} height={150}/>}
// topIcon={<img src={"/img/icon-amex.png"} width={100} height={100}/>}
// subText={"Credit Card"}
// subText={"American Express"}
value={"cc"}
active={paymentMethod === "cc"}
onClick={ method => {
this.setState({disableNext: true, paymentMethod: method },
() => this.handleSelectPaymentMethod(method)
)
} }
/>
<CustomButtonBox
userIsTouching={false}
sm={6} md={5}
height='92px'
topIcon={<img src={"https://www.anywhere.com/img-b/icons/icon-paypal.png"} width={150} height={50}/>}
// topIcon={<img src={"/img/visa-mastercard-paypal.jpeg"} width={180} height={70}/>}
// subText={"Visa, Mastercard, Discover, Paypal"}
value={"paypal"}
active={paymentMethod === "paypal"}
onClick={ method => {
this.setState({disableNext: true, paymentMethod: method, processing: true},
() => this.handleSelectPaymentMethod(method)
)
} }
/>
</Grid>
</>
const agreeToTerms = <>
<div className="mt-10">
<div className="relative flex items-start">
<div className="flex h-6 items-center">
<input
id="comments"
aria-describedby="comments-description"
name="comments"
type="checkbox"
checked={termsAck}
onClick={() => this.setState(pS => ({ termsAck: !pS.termsAck }))}
className="h-6 w-6 rounded border-gray-300 text-brand-blue focus:ring-indigo-600"
/>
</div>
<div className="ml-3 text-lg leading-6">
<label htmlFor="comments"
className={`font-medium ${termsAck ? 'text-gray-900' : 'text-red-600'}`}>
I Agree to The Following:
{!termsAck && <span className="text-red-600 font-light "> * required to continue</span>}
</label>
<p id="comments-description" className="text-gray-500">
I acknowledge that I have read and accept the
<a target="_blank" href={`/company/terms-of-service`} title="Anywhere.com Terms and Conditions"
className="decorate-brand-link pl-2 pr-2">
Terms and Conditions</a>
and the
<a target="_blank" href={`/company/privacy`} title="Anywhere.com Privacy Policy"
className="decorate-brand-link pl-2">
Privacy Policy</a>
</p>
<p id="comments-description" className="text-gray-500">
I will be billed {currencyFormat(totalPaymentAmount)}&nbsp;for
itinerary #{statement.itinerary_id}
{tipAmount > 0 ? ` (includes a ${currencyFormat(tipAmount)} tip 🙏)` : ''}
</p>
{(statement.itinerary.bill_date && paymentType !== "balance" && statement.balance_due_usd && statement.balance_due_usd - paymentAmount > 0 && !customText) &&
<p id="comments-description" className="text-gray-500">
Remaining balance of&nbsp;{currencyFormat(statement.balance_due_usd - paymentAmount)}&nbsp;will
be billed automatically on the&nbsp;{formatDate(statement.itinerary.bill_date, 'Do of MMMM YYYY')}
</p>}
</div>
</div>
</div>
</>
const paypalLoading = <>
{/* if(paymentMethod == "paypal"){ */}
<Grid container spacing={4} justifyContent="center">
<Grid item xs={12}>
<Typography align="center">
{_localize("Redirecting you to Paypal's payment Page", "Redirigiendote a la página de pago via Paypal")}
</Typography>
</Grid>
<Grid item>
{!errorMsg && <CircularProgress color="primary"/>}
</Grid>
<Grid item xs={12}>
<Typography display="inline">
{_localize(`If a popup doesn't open in the next 3 seconds, please click on this link: `, `Si no se abre una venta nueva, por favor haz click aquí para redigirte manualmente:`)}
</Typography>
<Button
color="secondary"
onClick={()=> {
// open new window in tab since popup blockers block window.open
const paypalWindow = window.open(paypalUrl, "_blank");
this.paypalWait(paypalWindow);
}}
>
Paypal Portal
</Button>
</Grid>
</Grid >
</>
const tokenizeForm = <TokenizeToCustomer
setLoading={(s) => this.setState({processing: s})}
setError={(e) => this.setState({errorMsg: e})}
amount={totalPaymentAmount}
onSuccessfulTokenization={(t) => {
console.log('onSuccessfulTokenization', t)
const { exp, number, type} = t.card;
// first 2 digits of t.card.exp is is MM
const expMonth = exp?.slice(0,2);
// last 2 digits of t.card.exp is is YY
const expYear = exp?.slice(2,4);
const cardType = type === "visa" ? "Visa"
: type === "mastercard" ? "Mastercard"
: type === "amex" ? "Amex"
: type === "discover" ? "Discover"
: type === "diners" ? "Diners"
: undefined
const ccInfo = new CreditCardInfo({...this.state.ccInfo,
cardNumber: number,
cardType: cardType,
expMonth: parseInt(expMonth),
expYear: parseInt(expYear),
token: t.token,
securityCode: '111' })
const paymentRequest = new PaymentRequest({
...this.state.paymentRequest,
tipAmount: tipAmount,
cardId: undefined,
securityCode: '111',
// carbonAmount: 0,
// reserveIds: this.state.reserveIds, // TODO
// ipAddress: this.state?.ipAddress
})
// console.log('CreditCardInfo', ccInfo)
// console.log('PaymentRequest', paymentRequest)
this.setState({processing: true, errorMsg: undefined})
// call the Endpoint
const { planFkey, userEmail, itineraryId } = this.props;
POST_pay_charge_with_token_by_id(
itineraryId,
{ email: userEmail,
baseAmount: paymentAmount,
plan_fkey: planFkey
}, [ paymentRequest, ccInfo ]
).catch(error => {
// handle error
console.log('POST_pay_charge_with_token_by_id', e)
const e = error?.cause?.headers?.get('x-any-error')
|| error?.data?.metadata?.generic?.constraint[0];
this.setState({errorMsg: e, processing: false})
}).then((r: L.Either<string, number>) => {
// handle success
console.log('POST_pay_charge_with_token_by_id', r)
if(r?.type == 'error') {
let e = r?.error;
var msg = `Something went wrong, contact our team with this code: ${e}`
// Test Decline: Set amount < 1 and use 5431111111111111
if (e === 'DECLINE') {
msg = `According to our bank, your card has declined or cannot be honored at this time.
It is best to contact your bank to find out why they declined this charge.
You can try another card if you prefer.`
}
// if(e === 'Invalid') {
// msg = `According to our bank, the card number you entered is not correct.
// Please double check each entry you made and before submitting again.
// You can try another card if you prefer.`
// } else if (e === 'Not Processed') {
// msg = `According to our bank, your card was not charged.
// The reason was unspecified but seems unrelated to your balance, so you should
// contact your bank to understand the reason.
// You can try another card if you prefer.`
// }
this.setState({errorMsg: msg})
} else {
this.setState({errorMsg: undefined, postbackId: r.success})
this.handleSendNotes();
analyticsEvent(itineraryId);
if(payWall) {
setTimeout(() => {
window.close();
}, 2000)
}
}
}).finally(() => {
this.setState({processing: false})
})
}}
>
{({onSubmit, onRestart, collect, response, reset}) => {
const disablePayNowButton =
!paymentMethod ||
processing ||
(!ccInfo || !ccInfo.billingAddress || !ccInfo.billingZIP || !ccInfo.cardholder) ||
(ccErrors.ccName || ccErrors.ccZip || ccErrors.billingAdd)
// || !response
return <Grid container spacing={2} justifyContent="center">
<>
{/* <button className="p-2 m-2" onClick={() => reset()} >Reset</button> */}
{/* <button className="p-2 m-2" onClick={(e) => onRestart(e)} >OnRestat</button> */}
{/* <button className="p-2 m-2" onClick={(e) => onSubmit(e)} >OnSubmit</button> */}
{/* <br /><pre> {JSON.stringify(response, null, 2)} </pre> */}
{/* <br /><pre> {JSON.stringify(collect, null, 2)} </pre> */}
{/* <pre>{JSON.stringify(ccErrors, null, 2)}</pre> */}
</>
<div className="px-3 w-full mt-10">
<div id="applepaybutton" className="applepaybutton"></div>
<div id="googlepaybutton" className="googlepaybutton"></div>
<label className="font-bold text-slate-600">Card Number</label>
<div id="ccnumber" className="w-full py-2 rounded-md h-16 block "></div>
<div className="flex flex-row space-x-3 h-20 ">
<div className="w-1/2">
<label className="font-bold text-slate-600">Card Expiration (MM/YY)</label>
<div id="ccexp" className="w-full py-2"></div>
</div>
<div className="w-1/2">
<label className="font-bold text-slate-600">Card CVV (security code)</label>
<div id="cvv" className="w-full py-2"></div>
</div>
</div>
<label htmlFor="name" className="block text-md font-bold leading-6 text-slate-600">
Name on Card
</label>
<div className="relative mt-2 rounded-md shadow-sm">
<input
type="text"
name="name"
id="name"
className={`block w-full rounded-sm border-0 py-2 pr-10 ring-1 ring-insetfocus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 ${
ccErrors.ccName ? 'border-red-300 text-red-900 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500'
: 'border-gray-300 placeholder-gray-400 focus:outline-none focus:ring-brand-blue focus:border-brand-blue'
}`}
placeholder="First Last"
value={ ccInfo.cardholder || ""}
onChange={(e : React.ChangeEvent<HTMLInputElement>) => {
const ccHold = e.target.value;
this.setState({
ccInfo: new CreditCardInfo({ ...ccInfo, cardholder: ccHold, token: ""}),
ccErrors: {...ccErrors, ccName: ccHold.length < 3 }
})
}}
/>
{ccErrors.ccName &&
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<ExclamationCircleIcon className="h-5 w-5 text-red-500" aria-hidden="true" />
</div>}
</div>
<div className="flex flex-row space-x-3 mt-4">
<div className="w-2/3">
{/* ADDRESS */}
<label htmlFor="address" className="block text-md font-bold leading-6 text-slate-600">
Billing Address
</label>
<div className="relative mt-2 rounded-md shadow-sm">
<input
type="text"
name="address" id="address"
placeholder="123 Main St."
className={`block w-full rounded-sm border-0 py-2 pr-10 ring-1 ring-insetfocus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 ${
ccErrors.billingAdd ? 'border-red-300 text-red-900 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500'
: 'border-gray-300 placeholder-gray-400 focus:outline-none focus:ring-brand-blue focus:border-brand-blue'
}`}
value={ccInfo.billingAddress || ""}
onChange={ (e : React.ChangeEvent<HTMLInputElement>) => {
this.setState({
ccInfo: new CreditCardInfo({ ...ccInfo, billingAddress: e.target.value, token: "" }),
ccErrors:{ billingAdd: e.target.value.length < 5 }
})
}}
/>
{ccErrors.billingAdd &&
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<ExclamationCircleIcon className="h-5 w-5 text-red-500" aria-hidden="true" />
</div>}
</div>
</div>
<div className="w-1/3">
{/* POSTAL CODE */}
<label htmlFor="zip" className="block text-md font-bold leading-6 text-slate-600">
ZIP / Postal Code
</label>
<div className="relative mt-2 rounded-md shadow-sm">
<input
type="text"
name="zip" id="zip"
placeholder="90210"
className={`block w-full rounded-sm border-0 py-2 pr-10 ring-1 ring-insetfocus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 ${
ccErrors.ccZip ? 'border-red-300 text-red-900 placeholder-red-300 focus:outline-none focus:ring-red-500 focus:border-red-500'
: 'border-gray-300 placeholder-gray-400 focus:outline-none focus:ring-brand-blue focus:border-brand-blue'
}`}
value={ccInfo.billingZIP || ""}
onChange={ (e : React.ChangeEvent<HTMLInputElement>) => {
this.setState({
ccInfo: new CreditCardInfo({ ...ccInfo, billingZIP: e.target.value, token: "" }),
ccErrors: {...ccErrors, ccZip: e.target.value.length < 2 }
})
}}
/>
{ccErrors.ccZip &&
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<ExclamationCircleIcon className="h-5 w-5 text-red-500" aria-hidden="true" />
</div>}
</div>
</div>
</div>
<div className="py-5">
{errorMsg && <Typography variant="body2" align="center" color="error" paragraph> {errorMsg} </Typography> }
<Button size="large" color="primary" variant="contained" fullWidth
id="payButton"
onClick={() => {
// TODO
console.log('pay now clicked')
this.setState({ processing: true })
}}
disabled={disablePayNowButton}
startIcon={<LockIcon /> }
>
{ processing ? <CircularProgress thickness={5.5} size={24} color="inherit"/> : "Pay Now"}
</Button>
</div>
</div>
</Grid>
}}
</TokenizeToCustomer>
const showHowWillYouPay = paymentType && termsAck && !postbackId && totalPaymentAmount > 1;
return <>
<Head>
<meta name="ROBOTS" content="NOINDEX, NOFOLLOW" />
<title>Pay Anywhere.com</title>
</Head>
{/* TODO - country specific contact number */}
<HomeAppBar showDefault />
{this.state?.collectJS &&
<CollectJSProvider collectJSPromise={this.state?.collectJS}>
<div className="max-w-lg min-w-sm mx-auto m-5">
{!postbackId &&
<div>
{howMuchWillYouPay}
{tipOptions()}
<br/>
{/* {finalAmountAndSchedule} */}
{confirmTripDetails}
{ (paymentType && totalPaymentAmount > 1) && agreeToTerms}
<br/>
</div>}
{ showHowWillYouPay &&
<div>
{howWillYouPay}
{paymentMethod == "cc" && tokenizeForm}
{paymentMethod == "paypal" && paypalLoading}
</div>
}
{postbackId > 0 && thankYou() }
</div>
</CollectJSProvider>}
</>
}
}
export default PaymentForm
const analyticsEvent = ( itineraryId: number ) => {
// Get value on 'admin' key of localStorage
const lsAdmin = window.localStorage && window.localStorage.getItem('admin');
if (lsAdmin !== null)
return;
GET_itinerary_transaction_analytics_by_id(itineraryId)
.then( nullableAmount => {
if(nullableAmount === null)
return;
try {
gtag( 'event', 'purchase', {
transaction_id: `I${itineraryId}`,
affiliation: " ",
value: nullableAmount,
currency: 'USD',
tax: 0,
shipping: 0,
items:[{
id: `I${itineraryId}`,
name: `Itinerary ${itineraryId}`,
price: nullableAmount && nullableAmount.toString(),
}]
})
POST_itinerary_transaction_analytics_by_id(itineraryId)
.catch(_err => sendToZapier({ props: itineraryId, e: _err }))
} catch (e) {
sendToZapier({ props: itineraryId, e, })
}
})
.catch( e=> sendToZapier({ props: itineraryId, e }))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment