Reopen and edit a saved design
Customers often want to come back to a design, whether from a cart, an account page or an email. Every design lives in a bundle, so reopening a design means reopening its bundle by ID.
This tutorial shows how to:
- Store the bundle ID when the customer finishes.
- Reopen the bundle from your site with the Hosted Experience API.
- Handle reopened bundles inside a theme.
1. Store the bundle ID
The bundle ID is returned when the customer finishes, in the result of the complete callback. Save it alongside the cart line items or the customer's account, next to each design's transactionId.
experience.on("complete", async (result) => {
// Save the bundle ID somewhere you can find it later, for example against
// the cart, the order or the customer.
await saveBundleId(result.bundleId);
// ... add result.items to the cart.
});
If you build your own experience with the Core SDK, bundle.getId() returns the same ID, and bundle.finish() resolves with a result that contains it.
2. Reopen the bundle with the Hosted Experience API
Launch a hosted experience as usual, but pass the saved ID as bundleId. The activation is still required, as it determines the theme the customer sees.
function reopenDesign(bundleId) {
const experience = new window.Spiff.HostedExperience({
bundleMode: true,
applicationKey: "<Your application key>",
presentmentCurrency: "USD",
bundleId: bundleId,
queryParams: { activationId: "<Your activation ID>" },
});
experience.on("complete", handleComplete);
experience.on("quit", handleQuit);
experience.execute();
}
If your account is not in the Australian region, call window.Spiff.setRegion before this, exactly as when launching a new experience.
When bundleId is set, the hosted experience loads that bundle instead of creating a new one. Options that only apply to new bundles, such as productCollectionId and initialBundleMetadata, are not needed.
Handle the result
The customer may change, add or remove designs, so treat the complete result as the new truth. It contains the bundle's current items, each with a transactionId. Replace the lines you previously added for this bundle with the new ones, and update the stored transaction IDs against them.
async function handleComplete(result) {
// Remove the cart lines previously created for this bundle.
await removeCartLinesForBundle(result.bundleId);
// Add the current designs.
await addLineItemsToCart(
result.items.map((item) => ({
productId: item.externalCartProductId,
variantId: item.externalCartProductVariantId,
quantity: item.quantity,
spiffBundleId: result.bundleId,
spiffTransactionId: item.transactionId,
imageUrl: item.previewImage,
})),
);
}
removeCartLinesForBundle and addLineItemsToCart stand in for your own cart code. See Launch your first experience from a custom storefront for the mapping.
3. Handle reopened bundles in a theme
If you build your own theme, you don't need to do anything to receive a reopened bundle beyond handling both bundle option types. When a customer reopens a bundle, the theme context has bundleOptions.type set to "existing" and a bundleId. When starting fresh, it is "new". See Theme Context and Loader Reference.
Load an existing bundle with client.getExistingBundle(bundleId) and a new one with client.getNewBundle.
import type { ThemeContextBundle } from "@spiffcommerce/theme-bridge";
import type { Bundle, SpiffCommerceClient } from "@spiffcommerce/core";
async function loadBundle(client: SpiffCommerceClient, context: ThemeContextBundle): Promise<Bundle> {
if (context.bundleOptions.type === "existing") {
// Reopen the customer's saved bundle.
return await client.getExistingBundle(context.bundleOptions.bundleId);
}
// Otherwise start a new bundle within the activation.
return await client.getNewBundle(context.bundleOptions.productCollectionId, undefined, {}, context.activation?.id);
}
getExistingBundle resolves once the bundle and its designs have loaded. The workflow experiences already in it are available from the bundle.
const bundle = await loadBundle(client, context);
// One workflow experience per design the customer has already created.
const experiences = bundle.getWorkflowExperiences();
From here, render the experiences exactly as you would for a new bundle. The customer can keep editing, add further designs with client.getWorkflowExperiences and bundle.addWorkflowExperiences, remove designs with bundle.removeWorkflowExperiences, and finish with bundle.finish() as before. The theme reference implementation in apps/corporate-theme/src/bundles/attemptLoad.ts shows a complete version of this logic.
The reference theme also saves the current bundle ID in the browser's local storage, so a customer who refreshes the page gets their bundle back even when the theme was launched as "new". If you do this, fall back to creating a new bundle when loading the saved one fails, for example because it was deleted.
4. Try it
- Launch an experience, create a design and finish it. Check that your
completehandler stores thebundleId. - Call
reopenDesignwith that ID. The experience should open with the design you just made. - Change something and finish again. Check that your cart has the updated design and the new
transactionId.
Next steps
- Read the Hosted Experience API reference for the
bundleIdoption and the result fields. - Learn about the bundle model in Bundles.
- Learn how orders are placed in Placing an order with the GraphQL API.