Skip to content

Extending UmbCheckout

UmbCheckout publishes Umbraco notifications around its data. A site can change or refuse data before it is saved, and change data after it is read, without changing UmbCheckout itself.

Register handlers in a composer as usual:

MyComposer.cs
public class MyComposer : IComposer
{
public void Compose(IUmbracoBuilder builder)
{
builder.AddNotificationAsyncHandler<OnShippingRateSavingNotification, StripeShippingRateIds>();
builder.AddNotificationAsyncHandler<OnShippingRatesLoadedNotification, HideInternalShippingRates>();
}
}
Data Before it is saved (change it, or cancel) After it is read (change what is returned)
Configuration OnConfigurationSavingNotification OnConfigurationLoadedNotification
Stripe settings OnStripeSettingsSavingNotification OnStripeSettingsLoadedNotification
Shipping rates OnShippingRateSavingNotification (create and update), OnShippingRateDeletingNotification OnShippingRatesLoadedNotification
Tax rates (licensed add-on) OnStripeTaxRateSavingNotification, OnStripeTaxRateDeletingNotification OnStripeTaxRatesLoadedNotification
Basket OnSessionSavingNotification OnSessionLoadedNotification
  • Saving notifications carry the object about to be saved (Target) and, for an update, the stored one (Existing).
  • Loaded notifications carry the object (Target) or, for shipping and tax rates, the list about to be returned (Items). Remove a rate from Items to hide it from lists and single reads alike (checkout and shop code). Editors still see and manage it in the backoffice.
  • The existing …Saved, …Deleted, basket and session notifications are unchanged.

Cancel with CancelOperation(...) and an Error or Warning event message. That message is what the person sees.

StripeShippingRateIds.cs
public sealed class StripeShippingRateIds : INotificationAsyncHandler<OnShippingRateSavingNotification>
{
public Task HandleAsync(OnShippingRateSavingNotification notification, CancellationToken cancellationToken)
{
if (!notification.Target.Value.StartsWith("shr_", StringComparison.Ordinal))
{
notification.CancelOperation(new EventMessage("Shipping", "Stripe shipping rate IDs start with shr_", EventMessageType.Error));
}
return Task.CompletedTask;
}
}

Nothing is saved.

  • In the backoffice, the error toast shows “Save cancelled” or “Delete cancelled” with your message.
  • On the shop, a cancelled basket change leaves the basket as it was, and puts your message in TempData[UmbCheckout.Shared.Consts.TempData.UmbCheckoutBasketChangeCancelled] for your views to show.
  • notification.Cancel = true also cancels, with a generic message.
HideInternalShippingRates.cs
public sealed class HideInternalShippingRates : INotificationAsyncHandler<OnShippingRatesLoadedNotification>
{
public Task HandleAsync(OnShippingRatesLoadedNotification notification, CancellationToken cancellationToken)
{
notification.Items.RemoveAll(rate => rate.Name.StartsWith("Internal", StringComparison.Ordinal));
return Task.CompletedTask;
}
}
  • Loaded changes are for what is shown and handed to code. They are never stored.
  • UmbCheckout reads the stored data, without the Loaded hooks, wherever it changes or charges data:
    • basket changes and checkout (checkout charges the stored basket, with every unit price re-priced from published content);
    • the backoffice screens that edit configuration, Stripe settings, shipping rates and tax rates;
    • uSync exports and imports.
  • Your own code can do the same with using (UmbCheckoutHooks.SuppressLoaded()) { ... } (namespace UmbCheckout.Shared.Notifications). UmbCheckoutHooks.SuppressSaving() turns the Saving hooks off in the same way.
  • Checkout records its Stripe session on the basket with the Saving hooks off, so a basket Saving handler can’t stop the paid basket being cleared.
  • Loaded handlers run on every other read, including frequent internal ones, so keep them cheap.
  • A handler that throws fails the operation. UmbCheckout doesn’t hide handler errors.

When a customer pays, the basket is cleared on the success page or, if they never come back to it, on their next visit (see Payment Flow / Lifecycle). After a basket is cleared because its checkout session was paid, UmbCheckout publishes OnPaidBasketClearedNotification, with the basket and the checkout session ID. Use it, for example, to send a follow-up email.

Whether a checkout session has been paid is answered by IPaidCheckoutSessions (UmbCheckout.Core). UmbCheckout.Stripe registers an implementation backed by the webhook’s paid-sessions table. A site can register its own implementation; UmbCheckout.Stripe then leaves it in place.

Webhook notifications must be safe to repeat

Section titled “Webhook notifications must be safe to repeat”

Stripe delivers each webhook event at least once. Once your handlers have completed for an event, a redelivery is acknowledged without running them again. If a handler throws, or the server stops while handlers run, Stripe’s retry runs all the handlers again. Write webhook notification handlers so that running them twice does no harm (for example, check whether you’ve already sent that order’s email). To keep the records of handled events elsewhere, replace IStripeWebhookEventStore (see Replacing the event store).

Every UmbCheckout service is a public interface registered in Umbraco’s dependency injection, so a site can read every entity through it, decorate it or replace it. When you mock or decorate a service, cover the overloads that take a CancellationToken: UmbCheckout calls those (see Cancellation tokens).

The configuration and the Stripe settings are cached in memory. A save clears the cache on the server that saved it and, through Umbraco’s distributed cache, on every other server of a load-balanced site. The cache is a public type, SettingsCache<TEntity> (UmbCheckout.Shared.Persistence), with TryGet, TrySet and Clear.

StripeKeyRejection (UmbCheckout.Stripe.Helpers) tells a Stripe failure caused by a rejected API key (HTTP 401 or 403) from any other. Stripe’s own message for a rejected key echoes the key’s last 4 characters, so use StripeKeyRejection.DetailFor(exception) to get a message that is safe to show or log: it is the fixed StripeKeyRejection.Detail for a rejected key, and Stripe’s message otherwise.