Skip to content

Instantly share code, notes, and snippets.

@davidmdem
Last active January 19, 2024 01:56
Show Gist options
  • Select an option

  • Save davidmdem/05994bd0880a30acf153bcbf6f0cf093 to your computer and use it in GitHub Desktop.

Select an option

Save davidmdem/05994bd0880a30acf153bcbf6f0cf093 to your computer and use it in GitHub Desktop.
A basic demonstration of the Blackbaud Checkout flow for Advanced Donation Forms.
<div id="checkout-adf">
<h5>
{{fund.Name}}
</h5>
<p>
{{fund.Description}}
</p>
<hr />
<div v-if="!confirmationHtml" style="float:left; width: 200px" >
<label>Amount</label>
<input v-model="fund.Amount" />
<label>First Name</label>
<input v-model="Donor.FirstName" />
<label>Last Name</label>
<input v-model="Donor.LastName" />
<label>Email</label>
<input v-model="Donor.EmailAddress" />
<label>Street Address</label>
<input v-model="Donor.Address.StreetAddress" />
<label>City</label>
<input v-model="Donor.Address.City" />
<label>State</label>
<input v-model="Donor.Address.State" />
<label>Country</label>
<input v-model="Donor.Address.Country" />
<label>State</label>
<input v-model="Donor.Address.PostalCode" />
<br/>
<br/>
<a href="#" @click.prevent="checkout">Checkout</a>
</div>
<div v-if="confirmationHtml" v-html="confirmationHtml"></div>
<div style="float:right; width: 750px">
<h6>Donation</h6>
<pre><code>{{Donation}}</code></pre>
<br />
<br />
<h6>Checkout Data</h6>
<pre><code>{{CheckoutData}}</code></pre>
<br />
<br />
<h6>Content Info</h6>
<pre><code>{{contentInfo}}</code></pre>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script src="https://dev.forms.iuf.iu.edu/checkout-adf.js"></script>
(function() {
/**
* A shared instance of `DonationService`.
*/
var ds = {};
/**
* Checkout Payment popup is loaded in this form.
*
* Normally, BBIS wants to have only a single `<form>` element per page.
* A `<form>` placed into the ADF's Design section will not
* be rendered. So we insert on the client side.
*
* Note the `data-disable-submit='true'` attribute. This is required for the `handleCheckoutComplete`
* method to fire. If it is not set the page will refresh with a transaction ID in the URL instead.
*/
function AppendCheckoutForm() {
var form =
"<form method='get' id=\"paymentForm\" data-formtype='bbCheckout' data-disable-submit='true' novalidate></form>";
$("body").append(form);
}
/**
* Helper function so I can treat the donation service calls like a Promise instead
* of using the success/error callbacks.
*
* These wrappers are *not* required. I am doing it here so that I can use async/await
* syntax. It makes the code in the handlers a lot more straight forward to follow.
*/
function NewPromise() {
var promise = $.Deferred();
promise.resolver = (data, error) => {
return error ? promise.reject(error.message) : promise.resolve(data);
};
return promise;
}
function GetContentInfo(partId) {
var promise = NewPromise();
ds.getADFEditorContentInformation(partId, promise.resolver, promise.resolver);
return promise;
}
function GetPublicKey() {
var promise = NewPromise();
ds.getCheckoutPublicKey(promise.resolver, promise.resolver);
return promise;
}
function ValidateDonation(donation) {
var promise = NewPromise();
ds.validateDonationRequest(donation, promise.resolver, promise.resolver);
return promise;
}
function CheckoutDonationCreate(donation) {
var promise = NewPromise();
ds.checkoutDonationCreate(donation, promise.resolver, promise.resolver);
return promise;
}
function CheckoutDonationCancel(donation) {
var promise = NewPromise();
ds.checkoutDonationCancel(donation, promise.resolver, promise.resolver);
return promise;
}
function CheckoutDonationComplete(donation) {
var promise = NewPromise();
ds.checkoutDonationComplete(donation, promise.resolver, promise.resolver);
return promise;
}
// Initialize a single Vue instance/application within the page.
var app = new Vue({
el: "#checkout-adf",
/**
* The object returned by `data` is bound to the view template inside #checkout-adf
* and is accessible in methods as children of `this`.
*/
data() {
return {
partId: null,
publicKey: null,
transactionToken: null,
confirmationHtml: null,
contentInfo: {},
// Hard coding a single fund for demonstration.
// This could be populated with an ajax call in `mounted` or selected from a list, etc.
fund: {
DesignationId: "35287a05-6672-4ddb-8345-236ea6e88ed5",
Name: "Indiana University Priority Needs",
Description:
"Gifts to this account will be used to support grants, awards, scholarships or any other purpose consistent with the mission of the Foundation or Indiana University.",
Amount: 20
},
// The inputs on the page bind their values directly to this model
// as the user fills out the form. You can pre-populate these values
// here for more rapid testing.
Donor: {
FirstName: "Test",
LastName: "Person",
EmailAddress: "",
Address: {
StreetAddress: "123 Fake St.",
City: "Bloomington",
State: "Indiana",
Country: "United States",
PostalCode: "47401"
}
}
};
},
/**
* Called when the Vue component first loads.
*/
async mounted() {
console.log("checkout-adf mounted", new Date().toISOString());
// Populate `this.partId` and initialize the DonationService.
this.partId = $(".BBDonationApiContainer").data("partid");
ds = new BLACKBAUD.api.DonationService(this.partId);
// Get configuration info for this specific ADF part.
// https://developer.blackbaud.com/bbis/reference/rest/#donationeditorpartid
this.contentInfo = await GetContentInfo(this.partId);
// Put a bbCheckout form on the page.
AppendCheckoutForm();
},
methods: {
/**
* Called when the user clicks `checkout`.
*/
async checkout() {
console.log("checkout start");
// Validate the donation object.
// https://developer.blackbaud.com/bbis/reference/rest/#partiddonationvalidatedonationrequest
var validationResponse = await ValidateDonation(this.Donation);
console.log("validationResponse", validationResponse);
if (validationResponse !== true) {
this.handleError("Could not validate donation object.");
return false;
}
// Get a public key required to bring up the BBPS payment modal.
// https://developer.blackbaud.com/bbis/reference/rest/#adfcheckoutpublickey
var publicKeyResponse = await GetPublicKey();
this.publicKey = JSON.parse(publicKeyResponse.Data).PublicKey;
console.log("received public key", this.publicKey);
// Setup payment modal with callbacks.
var checkout = new SecureCheckout(
this.handleCheckoutComplete,
this.handleCheckoutError,
this.handleCheckoutCancelled,
this.handleCheckoutLoaded
);
// Show the modal.
checkout.processCardNotPresent(this.CheckoutData);
},
/**
* When the checkout modal opens.
*/
async handleCheckoutLoaded(event) {
// Get transactionId from the modal Iframe URL
var url = $("#bbCheckoutPaymentIframe").prop("src");
var tid = BLACKBAUD.api.querystring.getQueryStringValue("t", url);
this.transactionToken = tid;
// Create the donation
// https://developer.blackbaud.com/bbis/reference/rest/#adfcheckoutcreate
await CheckoutDonationCreate(this.Donation);
},
/**
* After the payment is processed and the modal closes.
*
* @param {*} event
* @param {*} tranToken
*/
async handleCheckoutComplete(event, tranToken) {
$("#bbCheckoutOverlayIframe").show();
if (tranToken) {
this.transactionToken = tranToken;
console.log("handleCheckoutComplete", tranToken, this.Donation);
// Transitions the checkout item from Pending to Complete
// https://developer.blackbaud.com/bbis/reference/rest/#adfcheckoutcomplete
var checkoutCompleteResponse = await CheckoutDonationComplete(this.Donation);
console.log("completeResponse", checkoutCompleteResponse);
// Pull out the confirmation HTML to display to the user.
this.confirmationHtml = JSON.parse(checkoutCompleteResponse.Data).confirmationHTML;
} else {
this.handleError("Checkout complete did not have a transaction token.");
}
$("#bbCheckoutOverlayIframe").hide();
},
/**
* Error handler. Not doing much in this demo.
* @param {*} event
* @param {*} errorMsg
* @param {*} errorCode
*/
handleCheckoutError(event, errorMsg, errorCode) {
console.log("handleCheckoutError", errorMsg, errorCode, event);
this.handleError(errorMsg);
},
/**
* Cancel donation if user closed popup.
* @param {*} event
*/
async handleCheckoutCancelled(event) {
console.log("handleCheckoutCancelled", event);
var cancelResponse = await CheckoutDonationCancel(this.Donation);
console.log("cancelResponse", cancelResponse);
$(document).unbind("checkoutComplete");
$(document).unbind("checkoutLoaded");
$(document).unbind("checkoutError");
$(document).unbind("checkoutCancel");
},
/**
* Show errors/instructions to the user.
* @param {*} msg
*/
handleError(msg) {
$("#bbCheckoutOverlayIframe").hide();
console.log("handleError", msg);
}
},
/**
* These computed objects are what gets submitted to the REST endpoints.
* Thier values are populated as the form is completed.
*/
computed: {
// Used for validate, create, complete, delete.
Donation() {
return {
Donor: this.Donor,
Gift: {
Designations: [
{
DesignationId: this.fund.DesignationId,
Amount: this.fund.Amount
}
]
},
Origin: {
PageId: BLACKBAUD.netcommunity.PageID,
PageName: "Checkout ADF"
},
TokenId: this.transactionToken,
PartId: this.partId,
MerchantAccountId: this.contentInfo.MerchantAccountID,
BBSPReturnUri: window.location.href,
BBSPTemplateSitePageId: BLACKBAUD.netcommunity.PageID
};
},
// Used to call the payment api to open the checkout pop up with parameters.
// The parameter for `SecureCheckout.processCardNotPresent`.
CheckoutData() {
return {
ClientAppName: "BBIS",
IsEmailRequired: true,
IsNameVisible: true,
key: this.publicKey,
Amount: this.fund.Amount,
Cardholder: this.Donor.FirstName + " " + this.Donor.LastName,
BillingAddressFirstName: this.Donor.FirstName,
BillingAddressLastName: this.Donor.LastName,
BillingAddressCity: this.Donor.Address.City,
BillingAddressCountry: this.Donor.Address.Country,
BillingAddressLine: this.Donor.Address.StreetAddress,
BillingAddressPostCode: this.Donor.Address.PostalCode,
BillingAddressState: this.Donor.Address.State,
UseCaptcha: this.contentInfo.RecaptchRequired,
MerchantAccountId: this.contentInfo.MerchantAccountID,
PrimaryColor: this.contentInfo.PrimaryFontColor,
SecondaryColor: this.contentInfo.SecondaryFontColor,
FontFamily: this.contentInfo.FontType,
UseVisaCheckout: this.contentInfo.UseVisaPass,
UseMasterpass: this.contentInfo.UseMasterPass,
UseApplePay: this.contentInfo.UseApplePay
};
}
}
});
})();
@davidmdem
Copy link
Author

davidmdem commented Aug 13, 2019

Live Demo (this demo will probably not stay live for long)

Setup:

  1. Create an ADF page/part with wallet options checked.
  2. Upload or otherwise host the checkout-adf.js file and update the <script> tag at the bottom of checkout-adf-design.html.
  3. Paste contents of checkout-adf-design.html into the ADF part's Design section.

Notes:

  • This is for demo and educational purposes only.
  • Requires a modern browser that understands async/await (no IE, see above)
  • There is no real error handling or field validation.
  • Uses the Vue framework for code organization and model binding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment