Skip to content

Commit

Permalink
feat: implement optional async checkout muatations and status check
Browse files Browse the repository at this point in the history
  • Loading branch information
mcstover committed Feb 12, 2024
1 parent 922a543 commit 52f2d29
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 36 deletions.
73 changes: 48 additions & 25 deletions src/components/Checkout/CheckoutDropInPaymentWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ import * as Sentry from '@sentry/vue';
import checkoutUtils from '@/plugins/checkout-utils-mixin';
import braintreeDropInError from '@/plugins/braintree-dropin-error-mixin';
import { pollForCheckoutStatus } from '@/util/checkoutUtils';
import braintreeDepositAndCheckout from '@/graphql/mutation/braintreeDepositAndCheckout.graphql';
import braintreeDepositAndCheckoutAsync from '@/graphql/mutation/braintreeDepositAndCheckoutAsync.graphql';
Expand Down Expand Up @@ -339,39 +340,61 @@ export default {
},
})
.then(kivaBraintreeResponse => {
// extract transaction saga id or transaction id from response
const transactionResult = this.useAsyncCheckout
? kivaBraintreeResponse?.data?.shop?.doNoncePaymentDepositAndCheckoutAsync
: kivaBraintreeResponse?.data?.shop?.doNoncePaymentDepositAndCheckout;
if (this.useAsyncCheckout && typeof transactionResult !== 'object') {
pollForCheckoutStatus(this.apollo, transactionResult)
.then(checkoutStatusResponse => {
this.handleSuccessfulCheckout(checkoutStatusResponse?.receipt?.checkoutId);
}).catch(errorResponse => {
this.handleFailedCheckout([
{
error: errorResponse.errorCode,
message: `${errorResponse?.errorMessage}, ${errorResponse?.status}`,
}
]);
});
return kivaBraintreeResponse;
}
// Check for errors in transaction
if (kivaBraintreeResponse.errors) {
this.$emit('updating-totals', false);
this.processBraintreeDropInError('basket', kivaBraintreeResponse);
// Payment method failed, unselect attempted payment method
this.$refs.braintreeDropInInterface.btDropinInstance.clearSelectedPaymentMethod();
// Initialize a refresh of basket state
this.$emit('refreshtotals');
// exit
this.handleFailedCheckout(kivaBraintreeResponse);
return kivaBraintreeResponse;
}
// Transaction is complete
const transactionId = _get(
kivaBraintreeResponse,
'data.shop.doNoncePaymentDepositAndCheckout'
);
// redirect to thanks with KIVA transaction id
if (transactionId) {
// fire BT Success event
this.$kvTrackEvent(
'basket',
`${paymentType} Braintree DropIn Payment`,
'Success',
transactionId,
transactionId
);
// Complete transaction handles additional analytics + redirect
this.$emit('complete-transaction', transactionId);
}
this.handleSuccessfulCheckout(transactionResult, paymentType);
return kivaBraintreeResponse;
});
},
handleSuccessfulCheckout(transactionId, paymentType) {
// redirect to thanks with KIVA transaction id
if (transactionId) {
// fire BT Success event
this.$kvTrackEvent(
'basket',
`${paymentType} Braintree DropIn Payment`,
'Success',
transactionId,
transactionId
);
// Complete transaction handles additional analytics + redirect
this.$emit('complete-transaction', transactionId);
}
},
handleFailedCheckout(kivaBraintreeResponse) {
this.$emit('updating-totals', false);
this.processBraintreeDropInError('basket', kivaBraintreeResponse);
// Payment method failed, unselect attempted payment method
this.$refs.braintreeDropInInterface.btDropinInstance.clearSelectedPaymentMethod();
// Initialize a refresh of basket state
this.$emit('refreshtotals');
},
},
};
</script>
40 changes: 30 additions & 10 deletions src/components/Checkout/KivaCreditPayment.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<script>
import checkoutUtils from '@/plugins/checkout-utils-mixin';
import { pollForCheckoutStatus } from '@/util/checkoutUtils';
import KvButton from '~/@kiva/kv-components/vue/KvButton';
export default {
Expand Down Expand Up @@ -47,29 +48,48 @@ export default {
}
}).catch(errorResponse => {
this.$emit('updating-totals', false);
console.error(errorResponse);
this.showCheckoutError(errorResponse);
});
},
checkoutCreditBasket() {
this.checkoutBasket(false, this.useAsyncCheckout)
.then(transactionResult => {
if (typeof transactionResult !== 'object') {
// succesful validation
this.$kvTrackEvent('basket', 'Kiva Checkout', 'Success', transactionResult);
// Complete transaction handles additional analytics + redirect
this.$emit('complete-transaction', transactionResult);
if (this.useAsyncCheckout && typeof transactionResult !== 'object') {
pollForCheckoutStatus(this.apollo, transactionResult)
.then(checkoutStatusResponse => {
this.handleSuccessfulCheckout(checkoutStatusResponse?.receipt?.checkoutId);
}).catch(errorResponse => {
this.handleFailedCheckout([
{
error: errorResponse.errorCode,
message: `${errorResponse?.errorMessage}, ${errorResponse?.status}`,
}
]);
});
} else if (typeof transactionResult !== 'object') {
this.handleSuccessfulCheckout(transactionResult);
} else {
// checkout failed
this.$emit('updating-totals', false);
const errorResult = transactionResult?.errors ?? [];
this.showCheckoutError(errorResult);
this.$emit('checkout-failure', errorResult);
this.handleFailedCheckout(errorResult);
}
}).catch(errorResponse => {
this.$emit('updating-totals', false);
console.error(errorResponse);
this.handleFailedCheckout(errorResponse);
});
},
handleSuccessfulCheckout(transactionResult) {
// succesful validation
this.$kvTrackEvent('basket', 'Kiva Checkout', 'Success', transactionResult);
// Complete transaction handles additional analytics + redirect
this.$emit('complete-transaction', transactionResult);
},
handleFailedCheckout(errorResult) {
// checkout failed
this.$emit('updating-totals', false);
this.showCheckoutError(errorResult);
this.$emit('checkout-failure', errorResult);
}
}
};
</script>
3 changes: 3 additions & 0 deletions src/graphql/query/checkout/checkoutStatus.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ query checkoutStatus($transactionId: String!, $visitorId: String) {
basketId
errorCode
errorMessage
receipt {
checkoutId
}
requestedAt
status
transactionId
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/checkout-utils-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export default {
}
return new Promise((resolve, reject) => {
this.apollo.mutate(mutObj).then(data => {
const transactionId = _get(data, 'data.shop.checkout');
const transactionId = useAsync ? data?.data?.shop?.checkoutAsync : data?.data?.shop?.checkout;
if (transactionId !== null) {
// succesful transaction;
resolve(transactionId);
Expand Down
66 changes: 66 additions & 0 deletions src/util/checkoutUtils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import numeral from 'numeral';
import myFTD from '@/graphql/query/myFTD.graphql';
import checkoutStatus from '@/graphql/query/checkout/checkoutStatus.graphql';
import removeCreditByTypeMutation from '@/graphql/mutation/shopRemoveCreditByType.graphql';

/** Format Transaction Data for Analtyics events
Expand Down Expand Up @@ -120,3 +121,68 @@ export function removeCredit(apollo, creditType) {
}
});
}

/**
* Poll the checkoutStatus endpoint until the checkout is complete
* Note: We only operate on the COMPLETED or results with errors
*
* Possible status values:
* - BASKET_MANIFEST
* - BASKET_VALID
* - CHECKOUT_FAILED
* - CHECKOUT_RECORDED
* - CHECKOUT_ROLLED_BACK
* - COMPLETED
* - CREDIT_ADDED
* - DEPOSIT_RECORDED
* - FAILED
* - MANIFEST_FAILED
* - RECORD_CHECKOUT_FAILED
* - REQUEST_RECEIVED
* - RESERVATIONS_COMPLETED
* - STARTED
* - TRANSIENT_PAYMENT_METHOD_CHARGED
*
* @param {Object} apollo Apollo Client instance
* @param {Number} transactionId
* @param {Number} interval How often to poll
* @param {Number} timeout How long to allow polling to continue
* @returns {Promise}
*/
export async function pollForCheckoutStatus(
apollo = null,
transactionSagaId = 0,
interval = 1000,
timeout = 60000
) {
if (!apollo) {
throw new Error('Apollo instance missing');
}

// establish endtime based on timeout
const endTime = Date.now() + timeout;

const checkStatus = async () => {
// check for timeout
if (Date.now() > endTime) {
throw new Error('Polling timed out');
}
// query checkoutStatus
const result = await apollo.query({
query: checkoutStatus,
variables: {
transactionId: transactionSagaId
}
});
// extract fields to check for a completed status or errors
const { status, errorCode, errorMessage } = result?.data?.checkoutStatus;
// Check for completed status, or errors and return if present
if (status === 'COMPLETED' || errorCode || errorMessage) {
return result?.data?.checkoutStatus;
}
// Check again
setTimeout(checkStatus, interval);
};

return checkStatus();
}

0 comments on commit 52f2d29

Please sign in to comment.