Lazy loading and pickup¶
Many shop modules load the widget only when a shopper first clicks the "Try on" button. That saves a request on every page view, but it breaks one thing: a try-on that was sent on one page must be picked up on the next page, and the widget can only do that once it is loaded. This page explains the problem and the small loader that solves it.
The widget's own options and events are described in the Widget reference.
Why it matters¶
A try-on that has been sent is paid for, and the widget keeps following it when the shopper closes the window or moves
to another page of the same tab (see "A try-on outlives its window" in the Widget reference). The widget
keeps a small record of it in the tab's sessionStorage. The next page finds that record when TryOn.init() runs.
If your module loads the widget only on the first click, then on the next page nothing runs init(), so:
- the shopper who moved to another product sees no sign that their try-on finished;
- the result stays unclaimed until they happen to click the button again.
The fix is to load the widget at once on any page where the tab holds a record for your key, and to keep loading it lazily everywhere else. To do that you need to know whether such a record exists without loading the widget. That is what the loader is for.
The loader¶
The loader is a few kilobytes (ES5, no dependencies) and is served next to the widget with the same caching. It adds
one global object, ZiexTryOn, with two functions and a version property (the loader's own version; the widget's is
TryOn.version).
ZiexTryOn.hasPending(pk)¶
Returns true when the current tab holds a record of a try-on sent with this publishable key: the try-on is still
running, or it has finished and its outcome has not been shown yet. It returns false otherwise, including when
sessionStorage is not available. It reads storage only; it makes no request and loads nothing.
ZiexTryOn.load([scriptUrl])¶
Loads the widget script once and returns a promise that resolves with window.TryOn. Repeated calls share the same
load and resolve with the same object, whatever their argument. Without an argument it loads tryon-embed.js from the
loader's own directory, which is https://api.ziex-tryon.com/embed/tryon-embed.js when you print the loader as shown
above (and also when an optimiser has inlined or renamed the loader); pass scriptUrl only if you serve the widget from
another address. Do not print tryon-embed.js yourself as well.
If the script cannot be loaded (an HTTP error, a network failure, a script without TryOn) or does not arrive within
30 seconds, the promise is rejected, so add a .catch() if you want to react. The next call tries again; after a
timeout it waits for the same script tag, so the widget never runs twice. The widget needs Promise; in a browser
without it load() throws.
load() does not call init(). That stays with your page, so you decide the configuration.
The pattern¶
Print the loader on every page that can show the button. On each page, ask hasPending() and load at once when it is
true; otherwise load on the first click.
<button type="button" id="my-tryon-button" class="my-tryon-button">Try on</button>
<script src="https://api.ziex-tryon.com/embed/tryon-loader.js"></script>
<script>
(function () {
var KEY = 'pk_YOUR_PUBLISHABLE_KEY';
var config = {
apiKey: KEY,
launcher: false, // your own button opens the window
lockCategory: true,
category: 'tops',
productImage: 'https://shop.example/img/blue-shirt.jpg',
productTitle: 'Blue shirt',
productUrl: 'https://shop.example/blue-shirt',
integration: 'my-shop-module/1.0.0'
};
var ready = null;
function widget() {
if (!ready) {
ready = ZiexTryOn.load().then(function (TryOn) {
TryOn.init(config); // init() also picks up a sent try-on
return TryOn;
});
}
return ready;
}
// A try-on sent on an earlier page of this tab: load now, so it is picked up.
if (ZiexTryOn.hasPending(KEY)) widget();
// Everywhere else: load on the first click.
document.getElementById('my-tryon-button').addEventListener('click', function () {
widget().then(function (TryOn) { TryOn.open(); });
});
})();
</script>
Things to get right:
- Call
init()with the same publishable key on every page. The record belongs to a key; a page that uses another key does not see it. - Call
init()on every page wherehasPending()is true, even when the page does not show a product. The pickup is done byinit(). init()does not open the window. The shopper opens it, or your page does, and the outcome is shown then.- A single-page site that switches products should call
TryOn.update(), notdestroy()andinit(); seeupdate()ordestroy().
Telling the shopper that a result is waiting¶
While an outcome waits for the next opening of the window, the widget does two things your button can use.
1. A flag on <html>. data-tryon-ready is set to result or error. It stays set on every later page of the tab
until the shopper opens the window and sees the outcome. With launcher: false the widget draws no button, so style your
own:
.my-tryon-button { position: relative; }
html[data-tryon-ready] .my-tryon-button::after {
content: "";
position: absolute;
top: -3px;
right: -3px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #16a34a; /* a result is ready */
}
html[data-tryon-ready="error"] .my-tryon-button::after {
background: #dc2626; /* the try-on failed */
}
Because the flag is plain CSS on the root element, the badge appears even before any script of yours has run on the page.
2. The ready event. It is emitted once, on the page where the outcome arrived, after the outcome's own
upload_completed, result or error events:
TryOn.on('ready', function (e) {
// e = { job_id: '…', kind: 'result' | 'error', background: true }
console.log('Try-on outcome waiting:', e.kind);
});
Later pages do not emit it again; they show the flag only. Use the flag for the page-to-page state and the event for one-off reactions such as analytics.
What is stored, and for how long¶
- The record lives in the tab's
sessionStorage. It is not shared with other tabs, and it is gone when the tab is closed. A closed tab means the result is visible only in the client portal and the job history, not on your site. - It holds the idempotency key of the sent try-on, the job id once known, when it was sent, the estimate, the category and the product (name, link, picture address). It never holds the shopper's photo or your API key.
- It is removed when the outcome is shown, when the widget confirms that nothing was created, after
attemptMaxMs(45 minutes by default), or when you callTryOn.destroy().
Use the loader, do not copy the rule
The record's name is derived from the key by a rule inside the widget. Do not re-implement it in your module to
check for a record yourself. Call ZiexTryOn.hasPending(). The loader and the widget are released together, so the
answer always matches what the widget would find.
What is frozen in version 1¶
These parts of the contract do not change within version 1, so a module that relies on them keeps working:
- the storage slot in
sessionStorage: the nameziex-tryon:attempt:followed by the FNV-1a (32-bit) hash of the publishable key, written as 8 lower-case hexadecimal digits; - the record's version,
v: 1; - the flag
html[data-tryon-ready]with the valuesresultanderror; - the
readyevent and its data{ job_id, kind, background }; - the loader's interface:
ZiexTryOn.hasPending(pk)andZiexTryOn.load([scriptUrl]).
A change to any of them is made only as a new version of the record that old and new code both read, and it is announced
at least 90 days in advance. The version of the widget itself is TryOn.version, a semver string, and its options and
events only grow within a major version.