Skip to content

Confetti and a share card on finish

Goal: the moment the quiz finishes, confetti falls over the page and the shopper gets a “My result” image they can share or save.

Uses: Storefront JS API, quiz.finished (terminal_page, points, quiz_name). Nothing to install; no API key. Events fire on the Plus and Enterprise plans.

quiz.finished fires once per session, after the result page landed. A burst of a few dozen coloured squares is enough; they remove themselves.

<script>
window.octaneai = window.octaneai || [];
window.octaneai.push(function (octaneai) {
octaneai.on('quiz.finished', function () {
var colors = ['#f43f5e', '#f59e0b', '#10b981', '#3b82f6', '#a855f7'];
for (var i = 0; i < 60; i++) {
var piece = document.createElement('span');
piece.className = 'octane-confetti';
piece.style.left = Math.random() * 100 + 'vw';
piece.style.background = colors[i % colors.length];
piece.style.animationDelay = Math.random() * 600 + 'ms';
document.body.appendChild(piece);
piece.addEventListener('animationend', function () { this.remove(); });
}
});
});
</script>
<style>
.octane-confetti {
position: fixed; top: -12px; width: 10px; height: 14px; z-index: 2147483646;
pointer-events: none; animation: octane-confetti-fall 1800ms ease-in forwards;
}
@keyframes octane-confetti-fall {
to { transform: translateY(105vh) rotate(720deg); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .octane-confetti { display: none; } }
</style>

points maps each dimension id to its total, so the winning dimension is the largest entry; terminal_page is the key of the result page. Both are ids from the editor, not words a shopper should read, so keep a small map from id to label on the page. The card is drawn on a canvas and handed to the browser’s share sheet when it can take a file, else offered as a download.

<button id="share-result" hidden>Share my result</button>
<script>
window.octaneai = window.octaneai || [];
window.octaneai.push(function (octaneai) {
var LABELS = { dry: 'Dry skin', oily: 'Oily skin', combination: 'Combination skin' };
function winner(points) {
var ids = Object.keys(points);
ids.sort(function (a, b) { return points[b] - points[a]; });
return ids[0] || null;
}
function draw(quizName, headline) {
var canvas = document.createElement('canvas');
canvas.width = 1080; canvas.height = 1080;
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#111827'; ctx.fillRect(0, 0, 1080, 1080);
ctx.fillStyle = '#f9fafb'; ctx.textAlign = 'center';
ctx.font = '48px system-ui, sans-serif'; ctx.fillText(quizName, 540, 420);
ctx.font = 'bold 96px system-ui, sans-serif'; ctx.fillText(headline, 540, 560);
ctx.font = '36px system-ui, sans-serif'; ctx.fillText(location.hostname, 540, 980);
return canvas;
}
octaneai.on('quiz.finished', function (event) {
var result = event.data.object;
var id = winner(result.points) || result.terminal_page;
var headline = LABELS[id] || 'My result';
var canvas = draw(result.quiz_name || 'Quiz', headline);
var button = document.getElementById('share-result');
button.hidden = false;
button.onclick = function () {
canvas.toBlob(function (blob) {
var file = new File([blob], 'my-result.png', { type: 'image/png' });
if (navigator.canShare && navigator.canShare({ files: [file] })) {
navigator.share({ files: [file], title: headline }).catch(function () {});
return;
}
var link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'my-result.png';
link.click();
}, 'image/png');
};
});
});
</script>

The card carries the result label and the store’s hostname, nothing about the shopper: no answers, no email, no session id.

  • navigator.share rejects when the shopper dismisses the sheet; the .catch keeps that silent. It also needs a user gesture, which is why the share happens on the button’s click and not inside the event listener.
  • points is {} on a quiz without dimensions; the card then falls back to terminal_page, and to “My result” when that key has no label.
  • Sharing a file needs navigator.canShare({ files }): Safari on iOS and macOS, Chrome on Android and desktop; Firefox on desktop has no share sheet, so it takes the download path.
  • A shopper who retakes the quiz starts a new session and quiz.finished fires again; the button’s handler is replaced with the new card.
  • prefers-reduced-motion turns the confetti off; keep the share card, it does not move.
  • To see what the finish carries, set localStorage['octaneai:debug'] = '1' in the console.