Mystery Box

A custom HTML template for Mystery Box in-app messages. Copy the code, edit the code, and publish.

Mystery Box gives the user a choice. Two to four wrapped gift boxes sit on a sunburst background, the user taps one, it shakes, the lid pops off, and the reward appears with a code they can copy. It is best suited for promotional campaigns such as win-back journeys, loyalty milestones, festive gifting, and re-engagement pushes where the promise of a surprise is the hook.

Configure Mystery Box using Custom HTML Code

📘

This Is a Code Template, Not a Product Feature

  • Mystery Box is not a widget you enable in the Netcore dashboard, and it will not appear in any menu. It is a block of HTML that you copy from this page and paste into the Code of HTML Editor
  • Netcore does not host, version, or update this template on your behalf.

You are copying code out of this page and pasting it into a html code editor for campaign. Follow these steps in order.

  1. Create an in-app template. See Custom HTML for the full template creation flow.

  2. Copy the entire code block from the Sample HTML Code section on this page, from the first line to the last. Partial copies will not work.

  3. Paste it into the Code Editor and select Validate.

  4. Edit only the CONFIG block at the top of the code. This is where your rewards, colors, copy, and deeplink go. See Editable and Non Editable Section in Custom HTML Code before you change anything.

  5. Replace the placeholder deeplink. The template ships with netcore://your-deeplink-here, which does nothing. Set claimButton.url to a real deeplink registered in your app.

  6. Preview the message in the right pane, then schedule and publish.

📘

Set Up the Underlying Offer Separately

This template displays a reward code and copies it to the user's clipboard. It does not create coupons, apply discounts, or deduct inventory. Create the actual offer in your commerce platform or in Coupon

🚧

The Box the User Picks Does Not Change the Reward

The reward is drawn from the weighted rewards list the moment any box is tapped. All boxes are identical. Avoid copy suggesting that one box is better, that the user should choose carefully, or that the pick reveals anything about them.

Sample HTML Code

<!DOCTYPE html>
<html lang="en">

<head>
  <!-- CUSTOMIZE ZONE - Configurable Settings -->
  <!-- Everything inside CONFIG is safe to edit. This is the ONLY -->
  <!-- part a marketer/non-developer needs to touch. -->
  <!-- Keep this file ASCII-only: no emoji, no smart quotes, no -->
  <!-- en/em dashes, no currency symbols. The Smartech SDK base64 -->
  <!-- encodes the creative and btoa() breaks on non-Latin1 chars. -->
  <script>
    const CONFIG = {
      // Show the X close button (true = show, false = hide)
      includeDismissButton: true,

      // Background gradient of the card
      background: "linear-gradient(180deg, #7B2AA8 0%, #6A1E96 55%, #52147A 100%)",

      // Sunburst rays behind the boxes
      //   color       - ray colour (semi-transparent white reads best)
      //   rayCount    - how many rays (8 to 24 looks good)
      //   spin        - slowly rotate the sunburst (true / false)
      //   spinSeconds - seconds for one full rotation (higher = slower)
      sunburst: {
        enabled: true,
        color: "rgba(255, 255, 255, 0.07)",
        rayCount: 12,
        spin: true,
        spinSeconds: 70,
      },

      // Fire confetti when the reward is revealed
      enableConfettiOnWin: true,

      // The mystery boxes
      //   count        - how many boxes to show (2, 3 or 4)
      //   bodyColor    - the box colour
      //   lidColor     - the lid colour
      //   ribbonColor  - the ribbon and bow colour
      //   hint         - small text under the boxes before a pick
      //   openingHint  - replaces hint once a box is picked
      //   shakeSeconds - how long the picked box shakes before opening
      boxes: {
        count: 3,
        bodyColor: "#E8172F",
        lidColor: "#F4384E",
        ribbonColor: "#FFC3D2",
        hint: "Tap a box to open it",
        openingHint: "Opening your gift...",
        shakeSeconds: 0.7,
      },

      // Seconds between the lid popping off and the reward screen
      revealDelaySeconds: 1.1,

      // Claim button (shown on the reward screen)
      claimButton: {
        enabled: true,
        text: "Claim Reward",
        background: "linear-gradient(180deg, #FF8A3D 0%, #FC5E02 100%)",
        color: "#ffffff",
        // Netcore deeplink fired on claim. Replace with your app deeplink.
        url: "netcore://your-deeplink-here",
        // OPTIONAL extra data sent to the app alongside the click.
        payload: { campaign: "mystery_box" },
      },

      // Main headline (supports HTML like <br>)
      mainTitle: {
        text: "Pick a gift for a chance to win <br>a prize for your next order!",
        fontSize: "22px",
        color: "#ffffff",
      },

      // Subtitle / description
      subtitle: {
        text: "You get one pick. Every box has a real reward inside.",
        fontSize: "13px",
        color: "#E4D3F2",
      },

      // Copy-instruction text colors
      copyInstruction: {
        color: "#606060",
        copiedColor: "#8E24AA",
      },

      // Possible rewards (one is picked at random by weighting)
      //   title       - headline shown on the reward screen
      //   value       - coupon / code copied on click
      //   subtitle    - small description shown on the reward screen
      //   probability - weighting (does NOT need to sum to 1)
      rewards: [
        {
          title: "Nice pick! Your next order ships free.",
          value: "SHIPFREE",
          subtitle: "Use SHIPFREE at checkout for free delivery on your next order. No minimum cart value. Valid for 7 days.",
          probability: 0.34,
        },
        {
          title: "Boom! Rs. 300 off your next order.",
          value: "GIFT300",
          subtitle: "Use GIFT300 at checkout for flat Rs. 300 off on orders above Rs. 1,499. Valid for 7 days.",
          probability: 0.24,
        },
        {
          title: "You unlocked a free mystery gift!",
          value: "MYSTERYGIFT",
          subtitle: "Use MYSTERYGIFT to add a surprise gift free to your next order above Rs. 999. Valid for 10 days.",
          probability: 0.18,
        },
        {
          title: "Double points, coming your way.",
          value: "DOUBLE2X",
          subtitle: "Use DOUBLE2X to earn 2X loyalty points on your next order. Valid for 7 days.",
          probability: 0.18,
        },
        {
          title: "JACKPOT! Buy 1 Get 1 on bestsellers.",
          value: "B1G1STAR",
          subtitle: "Use B1G1STAR to pick any two bestsellers and pay for one. Valid for 3 days, while stocks last.",
          probability: 0.06,
        },
      ],
    };
  </script>
  <!-- END CUSTOMIZE ZONE -->

  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Mystery Box - Pick and Win!</title>

  <!-- DO NOT MODIFY - Netcore Smartech In-App SDK wiring -->
  <!-- Order matters: jQuery then Smartech App SDK then init(). -->
  <script src="https://code.jquery.com/jquery-3.4.1.min.js"
    integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
  <script src="https://cdnt.netcoresmartech.com/smartech-app-sdk/scripts/prod/smartech-app-sdk.js?v=1.0.1"></script>
  <script>
    SmartechAppSDK.init();
  </script>
  <!-- END SDK wiring -->

  <style>
    :root {
      --shimmer-hue-1: 285deg;
      --shimmer-hue-2: 300deg;
    }

    @property --mask {
      syntax: "<angle>";
      inherits: false;
      initial-value: 33deg;
    }

    @keyframes spin {
      0% { --mask: 0deg; }
      100% { --mask: 360deg; }
    }

    html,
    body {
      height: 100%;
      width: 100%;
      margin: 0;
      display: flex;
      align-items: center;
      justify-content: space-evenly;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
        Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
    }

    .modal {
      display: flex;
      flex-direction: column;
      align-items: center;
      height: 100%;
      max-height: 580px;
      max-width: 320px;
      padding: 0 5%;
      justify-content: space-evenly;
      width: 100%;
      color: #ffffff;
      text-align: center;
      background: linear-gradient(180deg, #7B2AA8 0%, #6A1E96 55%, #52147A 100%);
      border-radius: 36px;
      position: relative;
      box-sizing: border-box;
      overflow: hidden;
    }

    /* Both screens keep the SAME max-height. Do not cap the reward
       screen lower (455px / 560px) - that causes a visible card
       resize the moment the user moves between screens. */
    .modal.reward { max-height: 580px; }

    .close-btn {
      position: absolute;
      top: 18px;
      right: 18px;
      appearance: none;
      border: 0;
      background: none;
      color: #ffffff;
      font-size: 32px;
      line-height: 1;
      cursor: pointer;
      z-index: 20;
    }

    h1 { margin: 0; font-size: 22px; font-weight: 700; position: relative; z-index: 2; }
    p { margin: 0; font-size: 13px; line-height: 1.45; position: relative; z-index: 2; }

    .title, .reward-title { margin-top: 30px; }
    .reward-title { font-size: 22px; font-weight: 700; }

    /* Sunburst - triangles built from CSS borders, no clip-path,
       no 3D transforms. The wrapper is what rotates; each ray keeps
       its own static rotate() so the animation cannot overwrite it. */
    .rays-wrap {
      position: absolute;
      top: 50%;
      left: 50%;
      width: 0;
      height: 0;
      pointer-events: none;
      z-index: 0;
    }

    .rays-wrap.spinning { animation: rays-spin linear infinite; }

    @keyframes rays-spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }

    .rays-wrap i {
      position: absolute;
      top: 0;
      left: 0;
      width: 0;
      height: 0;
      border-left: 70px solid transparent;
      border-right: 70px solid transparent;
      border-bottom: 460px solid transparent;
      margin-left: -70px;
      transform-origin: 50% 0;
      display: block;
    }

    /* Gift boxes */
    .boxes {
      display: flex;
      align-items: flex-end;
      justify-content: center;
      gap: 10px;
      position: relative;
      z-index: 2;
    }

    .box {
      position: relative;
      width: 78px;
      height: 104px;
      cursor: pointer;
      transition: opacity .35s ease, filter .35s ease;
    }

    .box.dimmed { opacity: .3; filter: grayscale(.35); }
    .box.opened { cursor: default; }

    .boxes.locked .box { pointer-events: none; }

    .box.shaking { animation: box-shake .14s linear infinite; }

    @keyframes box-shake {
      0%   { transform: translateX(0) rotate(0deg); }
      25%  { transform: translateX(-3px) rotate(-3deg); }
      50%  { transform: translateX(0) rotate(0deg); }
      75%  { transform: translateX(3px) rotate(3deg); }
      100% { transform: translateX(0) rotate(0deg); }
    }

    .box-body {
      position: absolute;
      left: 4px;
      right: 4px;
      bottom: 0;
      height: 66px;
      border-radius: 5px;
      overflow: hidden;
    }

    .box-lid {
      position: absolute;
      left: 0;
      right: 0;
      bottom: 60px;
      height: 22px;
      border-radius: 4px;
      overflow: hidden;
      transition: transform .55s cubic-bezier(.34, 1.4, .5, 1), opacity .5s ease;
    }

    .box-body span,
    .box-lid span {
      position: absolute;
      top: 0;
      bottom: 0;
      left: 50%;
      width: 15px;
      margin-left: -7.5px;
    }

    .box-bow {
      position: absolute;
      left: 0;
      right: 0;
      bottom: 78px;
      height: 22px;
      transition: transform .55s cubic-bezier(.34, 1.4, .5, 1), opacity .5s ease;
    }

    .box-bow i {
      position: absolute;
      bottom: 0;
      width: 24px;
      height: 20px;
      border-radius: 60% 60% 40% 40%;
      display: block;
    }

    .box-bow i.l { left: 8px; transform: rotate(-24deg); }
    .box-bow i.r { right: 8px; transform: rotate(24deg); }

    .box-flash {
      position: absolute;
      left: 50%;
      bottom: 44px;
      width: 14px;
      height: 14px;
      margin-left: -7px;
      border-radius: 50%;
      background: #FFF6C9;
      opacity: 0;
      transition: transform .5s ease-out, opacity .5s ease-out;
      box-shadow: 0 0 22px 10px rgba(255, 233, 150, .85);
    }

    .box.opened .box-lid { transform: translateY(-46px) rotate(-16deg); }
    .box.opened .box-bow { transform: translateY(-50px) rotate(-16deg); }
    .box.opened .box-flash { opacity: 1; transform: scale(3.4); }

    .hint-text {
      font-size: 12px;
      font-weight: 600;
      letter-spacing: .3px;
      color: rgba(255, 255, 255, .72);
      position: relative;
      z-index: 2;
    }

    /* Reward ticket */
    .reward-wrapper {
      display: none;
      width: 100%;
      position: relative;
      border-radius: 18px;
      overflow: visible;
      z-index: 2;
    }

    .reward-wrapper.visible { display: block; }

    .reward-box {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 6px;
      padding: 16px 0;
      background: #ffffff;
      color: #1d1d1d;
      border-radius: 18px;
      width: 100%;
      box-sizing: border-box;
      cursor: pointer;
    }

    .reward-main {
      font-size: 24px;
      font-weight: 700;
      display: flex;
      align-items: center;
      gap: 3px;
    }

    .reward-subtext {
      padding: 0 20px;
      font-size: 14px;
      font-weight: 500;
      position: relative;
      z-index: 2;
    }

    .copy-instruction {
      font-family: 'Proxima Nova', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
        Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
      font-weight: 500;
      font-size: 13px;
      line-height: 100%;
      text-align: center;
    }

    .copy-btn {
      display: inline;
      appearance: none;
      border: none;
      background: none;
      cursor: pointer;
      font-size: 24px;
      line-height: 1;
      color: #1d1d1d;
      transition: transform 0.1s ease-in-out;
    }

    .copy-btn:active { transform: scale(0.9); }

    .action-btn {
      width: fit-content;
      padding: 12.5px 40px;
      border: none;
      border-radius: 14px;
      background: #ffffff;
      color: #1d1d1d;
      font-size: 16px;
      font-weight: 700;
      cursor: pointer;
      position: relative;
      z-index: 2;
    }

    .action-btn:disabled { opacity: 0.6; cursor: default; }

    @keyframes pulsate {
      0% { transform: scale(1); }
      50% { transform: scale(1.05); }
      100% { transform: scale(1); }
    }

    .pulsate-animation { animation: pulsate 1.5s 3; }

    .shimmer {
      position: absolute;
      inset: -40px;
      border-radius: inherit;
      mix-blend-mode: plus-lighter;
      pointer-events: none;
      mask-image: conic-gradient(from var(--mask, 0deg),
          transparent 0%, transparent 10%, black 36%, black 45%,
          transparent 50%, transparent 60%, black 85%, black 95%, transparent 100%);
      mask-size: cover;
      animation: spin 3s linear infinite both -0.5s;
      z-index: 10;
    }

    .shimmer::before, .shimmer::after {
      content: "";
      border-radius: inherit;
      position: absolute;
      inset: 40px;
      opacity: 1;
    }

    .shimmer::before {
      box-shadow: 0 0 2px 1px hsl(var(--shimmer-hue-1) 90% 58%),
        0 0 9px 6px hsl(var(--shimmer-hue-2) 70% 60%);
      z-index: -1;
    }

    .reward-wrapper.visible .shimmer { opacity: 1; visibility: visible; }
    .reward-wrapper.shimmer-active .shimmer { animation: spin 2s linear infinite; }
  </style>
</head>

<body>
  <!-- Play screen -->
  <div class="modal" id="playModal">
    <!-- data-smt-action="SMTInAppClose" lets the Netcore SDK close
         the message and record the close event automatically. -->
    <button class="close-btn" aria-label="Close" data-smt-action="SMTInAppClose">&times;</button>

    <div class="rays-wrap" id="playRays"></div>

    <h1 class="title" id="mainTitle"></h1>
    <p id="subtitle"></p>

    <div class="boxes" id="boxes"></div>

    <p class="hint-text" id="hintText"></p>
  </div>

  <!-- Reward screen -->
  <div class="modal reward" id="rewardModal" style="display: none;">
    <button class="close-btn" aria-label="Close" data-smt-action="SMTInAppClose">&times;</button>

    <div class="rays-wrap" id="rewardRays"></div>

    <h1 class="reward-title" id="rewardTitle"></h1>
    <div id="rewardSubtext" class="reward-subtext"></div>

    <div class="reward-wrapper" id="rewardWrapper">
      <span class="shimmer"></span>
      <div class="reward-box ticket" id="rewardBox">
        <div class="reward-main">
          <span id="rewardText"></span>
          <button id="copyBtn" class="copy-btn" title="Copy to clipboard">
            <svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
              <path fill-rule="evenodd" clip-rule="evenodd"
                d="M13 13.8444V17.8444C13 18.4444 12.6 18.8444 12 18.8444H3C2.4 18.8444 2 18.4444 2 17.8444V8.84442C2 8.24442 2.4 7.84442 3 7.84442H7V3.84442C7 3.24442 7.4 2.84442 8 2.84442H17C17.6 2.84442 18 3.24442 18 3.84442V12.8444C18 13.4444 17.6 13.8444 17 13.8444H13ZM12 13.8444H8C7.4 13.8444 7 13.4444 7 12.8444V8.84442H3V17.8444H12V13.8444ZM17 3.84442H8V12.8444H17V3.84442Z"
                fill="#4A4C4C" />
            </svg>
          </button>
        </div>
        <div class="copy-instruction">Click here to copy the code!</div>
      </div>
    </div>

    <button class="action-btn" id="claimButton">Claim Reward</button>
  </div>

  <script>
    (function () {
      // -------------------- Element refs --------------------
      var playModal = document.getElementById('playModal');
      var rewardModal = document.getElementById('rewardModal');
      var playRays = document.getElementById('playRays');
      var rewardRays = document.getElementById('rewardRays');
      var mainTitleEl = document.getElementById('mainTitle');
      var subtitleEl = document.getElementById('subtitle');
      var boxesEl = document.getElementById('boxes');
      var hintTextEl = document.getElementById('hintText');
      var claimButton = document.getElementById('claimButton');
      var rewardWrapper = document.getElementById('rewardWrapper');
      var rewardBox = document.getElementById('rewardBox');
      var rewardTitleEl = document.getElementById('rewardTitle');
      var rewardTextEl = document.getElementById('rewardText');
      var rewardSubtextEl = document.getElementById('rewardSubtext');
      var copyBtn = document.getElementById('copyBtn');
      var copyInstructionEl = document.querySelector('.copy-instruction');
      var closeButtons = document.querySelectorAll('.close-btn');
      var isPlaying = false;
      var revealed = false;
      var timers = [];

      // DO NOT MODIFY - Netcore SDK interaction helper
      // Fire a tracked In-App CLICK to the app (deeplink + optional data).
      // Uses the SDK when present; falls back to a plain redirect so the
      // template still works in a bare browser preview.
      function netcoreTrackClick(deeplink, payload) {
        try {
          if (window.SmartechAppSDK && typeof SmartechAppSDK.trackInAppClick === 'function') {
            SmartechAppSDK.trackInAppClick(deeplink, payload || null);
            return;
          }
        } catch (e) {}
        if (deeplink) window.location.href = deeplink;
      }
      // END SDK helper

      function later(fn, ms) {
        var id = setTimeout(fn, ms);
        timers.push(id);
        return id;
      }

      // -------------------- Apply Config --------------------
      if (CONFIG.background) playModal.style.background = CONFIG.background;
      if (CONFIG.background) rewardModal.style.background = CONFIG.background;

      if (!CONFIG.includeDismissButton && closeButtons.length > 0) {
        closeButtons.forEach(function (btn) { btn.style.display = 'none'; });
      }

      mainTitleEl.innerHTML = CONFIG.mainTitle.text;
      mainTitleEl.style.fontSize = CONFIG.mainTitle.fontSize;
      mainTitleEl.style.color = CONFIG.mainTitle.color;
      rewardTitleEl.style.color = CONFIG.mainTitle.color;

      subtitleEl.textContent = CONFIG.subtitle.text;
      subtitleEl.style.fontSize = CONFIG.subtitle.fontSize;
      subtitleEl.style.color = CONFIG.subtitle.color;
      rewardSubtextEl.style.color = CONFIG.subtitle.color;

      hintTextEl.textContent = CONFIG.boxes.hint;

      if (copyInstructionEl && CONFIG.copyInstruction.color) {
        copyInstructionEl.style.color = CONFIG.copyInstruction.color;
      }

      var rewards = CONFIG.rewards;

      // -------------------- Sunburst --------------------
      function buildRays(wrap) {
        if (!CONFIG.sunburst || !CONFIG.sunburst.enabled) {
          wrap.style.display = 'none';
          return;
        }
        var count = parseInt(CONFIG.sunburst.rayCount, 10);
        if (!count || count < 4) count = 12;
        var step = 360 / count;
        wrap.innerHTML = '';
        for (var i = 0; i < count; i++) {
          var ray = document.createElement('i');
          ray.style.borderBottomColor = CONFIG.sunburst.color;
          ray.style.transform = 'rotate(' + (i * step) + 'deg)';
          wrap.appendChild(ray);
        }
        if (CONFIG.sunburst.spin) {
          wrap.className = 'rays-wrap spinning';
          wrap.style.animationDuration = (CONFIG.sunburst.spinSeconds || 70) + 's';
        }
      }

      buildRays(playRays);
      buildRays(rewardRays);

      // -------------------- Build the boxes --------------------
      function buildBoxes() {
        var count = parseInt(CONFIG.boxes.count, 10);
        if (!count || count < 2) count = 2;
        if (count > 4) count = 4;

        boxesEl.innerHTML = '';
        for (var i = 0; i < count; i++) {
          var box = document.createElement('div');
          box.className = 'box';

          var flash = document.createElement('div');
          flash.className = 'box-flash';
          box.appendChild(flash);

          var bow = document.createElement('div');
          bow.className = 'box-bow';
          var bowL = document.createElement('i');
          bowL.className = 'l';
          bowL.style.background = CONFIG.boxes.ribbonColor;
          var bowR = document.createElement('i');
          bowR.className = 'r';
          bowR.style.background = CONFIG.boxes.ribbonColor;
          bow.appendChild(bowL);
          bow.appendChild(bowR);
          box.appendChild(bow);

          var lid = document.createElement('div');
          lid.className = 'box-lid';
          lid.style.background = CONFIG.boxes.lidColor;
          var lidRibbon = document.createElement('span');
          lidRibbon.style.background = CONFIG.boxes.ribbonColor;
          lid.appendChild(lidRibbon);
          box.appendChild(lid);

          var body = document.createElement('div');
          body.className = 'box-body';
          body.style.background = CONFIG.boxes.bodyColor;
          var bodyRibbon = document.createElement('span');
          bodyRibbon.style.background = CONFIG.boxes.ribbonColor;
          body.appendChild(bodyRibbon);
          box.appendChild(body);

          box.addEventListener('click', (function (el) {
            return function () { pick(el); };
          })(box));

          boxesEl.appendChild(box);
        }
      }

      buildBoxes();

      // -------------------- Reward selection --------------------
      function chooseReward() {
        var total = rewards.reduce(function (acc, r) { return acc + r.probability; }, 0);
        var rand = Math.random() * total;
        for (var i = 0; i < rewards.length; i++) {
          if (rand < rewards[i].probability) return rewards[i];
          rand -= rewards[i].probability;
        }
        return rewards[rewards.length - 1];
      }

      // -------------------- Pick logic --------------------
      function pick(boxEl) {
        if (isPlaying || revealed) return;
        isPlaying = true;

        boxesEl.classList.add('locked');

        var all = boxesEl.querySelectorAll('.box');
        all.forEach(function (b) {
          if (b !== boxEl) b.classList.add('dimmed');
        });

        var selectedReward = chooseReward();
        hintTextEl.textContent = CONFIG.boxes.openingHint || '';

        var shakeMs = (CONFIG.boxes.shakeSeconds || 0.7) * 1000;
        var delayMs = (CONFIG.revealDelaySeconds || 1.1) * 1000;

        boxEl.classList.add('shaking');

        later(function () {
          boxEl.classList.remove('shaking');
          boxEl.classList.add('opened');
          later(function () { showReward(selectedReward); }, delayMs);
        }, shakeMs);
      }

      // -------------------- Reward screen --------------------
      function showReward(reward) {
        revealed = true;
        isPlaying = false;

        rewardTitleEl.textContent = reward.title;
        rewardTextEl.textContent = reward.value;
        rewardSubtextEl.textContent = reward.subtitle;
        rewardWrapper.classList.add('visible');
        rewardWrapper.classList.add('shimmer-active');
        rewardBox.classList.add('pulsate-animation');

        var shimmerElement = rewardWrapper.querySelector('.shimmer');
        if (shimmerElement) {
          shimmerElement.style.animation = 'none';
          shimmerElement.offsetHeight;
          shimmerElement.style.animation = 'spin 2s linear infinite';
        }

        playModal.style.display = 'none';
        rewardModal.style.display = 'flex';

        if (CONFIG.claimButton.enabled) {
          claimButton.textContent = CONFIG.claimButton.text;
          claimButton.style.background = CONFIG.claimButton.background;
          claimButton.style.color = CONFIG.claimButton.color;
          claimButton.addEventListener('click', function () {
            netcoreTrackClick(CONFIG.claimButton.url, CONFIG.claimButton.payload);
          }, { once: true });
        } else {
          claimButton.style.display = 'none';
        }

        if (CONFIG.enableConfettiOnWin) fireConfetti();
      }

      // -------------------- Copy to clipboard --------------------
      function copyToClipboard() {
        var text = rewardTextEl.textContent;
        if (!text) return;
        if (navigator.clipboard && navigator.clipboard.writeText) {
          navigator.clipboard.writeText(text).then(showCopySuccess).catch(function () { fallbackCopy(text); });
        } else {
          fallbackCopy(text);
        }
      }

      function copyRewardText(e) {
        if (e && e.target === copyBtn || (e.target.closest && e.target.closest('#copyBtn'))) return;
        copyToClipboard();
      }

      copyBtn.addEventListener('click', function (e) {
        e.stopPropagation();
        copyToClipboard();
      });
      rewardWrapper.addEventListener('click', copyRewardText);

      function showCopySuccess() {
        copyInstructionEl.textContent = "Code copied!";
        copyInstructionEl.style.color = CONFIG.copyInstruction.copiedColor;
        later(function () {
          copyInstructionEl.textContent = "Click here to copy the code!";
          copyInstructionEl.style.color = CONFIG.copyInstruction.color;
        }, 1500);
      }

      function fallbackCopy(text) {
        var input = document.createElement('input');
        input.setAttribute('readonly', '');
        input.setAttribute('value', text);
        input.style.position = 'absolute';
        input.style.left = '-9999px';
        document.body.appendChild(input);
        input.select();
        try { document.execCommand('copy'); } catch (e) {}
        document.body.removeChild(input);
        showCopySuccess();
      }

      // -------------------- Confetti (self-contained, no library) ----
      // Internal functions are named confettiDraw / confettiUpdate so they
      // can never collide with another function in this scope.
      function fireConfetti() {
        var colors = ["#FFC700", "#FF61E6", "#FC5E02", "#41F4BC", "#943DFF", "#FFD724"];
        var particleCount = 120;
        var gravity = 0.3;
        var confettiCanvas = document.createElement('canvas');
        confettiCanvas.style.position = 'fixed';
        confettiCanvas.style.top = '0';
        confettiCanvas.style.left = '0';
        confettiCanvas.style.width = '100%';
        confettiCanvas.style.height = '100%';
        confettiCanvas.style.pointerEvents = 'none';
        confettiCanvas.style.zIndex = '9999';
        document.body.appendChild(confettiCanvas);

        var confCtx = confettiCanvas.getContext('2d');
        confettiCanvas.width = window.innerWidth;
        confettiCanvas.height = window.innerHeight;
        var particles = [];
        var randomRange = function (min, max) { return Math.random() * (max - min) + min; };

        for (var i = 0; i < particleCount; i++) {
          particles.push({
            x: confettiCanvas.width / 2,
            y: confettiCanvas.height * 0.6,
            w: randomRange(6, 12),
            h: randomRange(8, 16),
            angle: randomRange(0, 360),
            tiltAngleSpeed: randomRange(0.05, 0.12),
            color: colors[Math.floor(Math.random() * colors.length)],
            velocityX: randomRange(-9, 9),
            velocityY: randomRange(-16, -8),
            opacity: 1,
            decay: randomRange(0.0075, 0.015),
          });
        }

        var confettiDraw = function () {
          particles.forEach(function (p) {
            confCtx.save();
            confCtx.globalAlpha = Math.max(p.opacity, 0);
            confCtx.fillStyle = p.color;
            confCtx.translate(p.x, p.y);
            confCtx.rotate(p.angle * Math.PI / 180);
            confCtx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
            confCtx.restore();
          });
        };

        var confettiUpdate = function () {
          confCtx.clearRect(0, 0, confettiCanvas.width, confettiCanvas.height);
          particles.forEach(function (p, idx) {
            p.x += p.velocityX;
            p.y += p.velocityY;
            p.velocityY += gravity;
            p.opacity -= p.decay;
            p.angle += p.tiltAngleSpeed * 180 / Math.PI;
            if (p.opacity <= 0 || p.y > confettiCanvas.height + 20) particles.splice(idx, 1);
          });
          confettiDraw();
          if (particles.length) requestAnimationFrame(confettiUpdate);
          else confettiCanvas.remove();
        };

        confettiUpdate();
      }

      // -------------------- Cleanup --------------------
      function cleanup() {
        for (var i = 0; i < timers.length; i++) { clearTimeout(timers[i]); }
        timers = [];
        rewardWrapper.removeEventListener('click', copyRewardText);
      }

      closeButtons.forEach(function (btn) { btn.addEventListener('click', cleanup); });
      window.addEventListener('beforeunload', cleanup);
    })();
  </script>
</body>

</html>

Editable and Non Editable Section in Custom HTML Code

The template is split into two zones. Everything you are meant to change sits in the first one.

📁 Structure of the Template
<script>
const CONFIG = { ... }
</script>
✓ Editable section
Update your rewards, boxes, colors, text, deeplinks, and other campaign settings here.
<script src="jquery..."></script>
<script src="smartech-app-sdk..."></script>
✕ Do not edit
Loads the required libraries and Netcore SDK.
<style> ... </style>
✕ Do not edit
Defines the widget layout, box animations, and styling.
<body> ... </body>
✕ Do not edit
Contains the widget structure rendered to users.
<script> ...engine... </script>
✕ Do not edit
Controls the pick logic, box animations, reward selection, and SDK interactions.
📘

Important Point to Remember

Remember Changing Values Is Safe, Changing Key Names Is Not!

  • Inside the CONFIG block, edit the values on the right of each colon. Do not rename the settings themselves. Changing revealDelaySeconds: 1.1 to revealDelaySeconds: 2 is fine. Renaming it to revealDelay: 2 means the template can no longer find it.
  • Keep the punctuation intact too. A missing comma or quotation mark stops the whole message from rendering.
  • The code below the CONFIG block is what makes the boxes open, reveal, and report back to Netcore, which is why DO NOT EDIT it.
🚧

Keep the File ASCII-only

Do not use emoji, smart quotes, en or em dashes, or currency symbols anywhere in this file. The creative is base64-encoded before delivery and the encoder fails on characters outside Latin-1. Where you need a symbol, use a unicode escape such as \uD83C\uDF89 instead of pasting the character.

Components You Must Not Remove

Five parts of the locked zone are worth knowing by name, because removing them is the most common way a campaign breaks.

ComponentWhy it's required
SDK wiring block (<head>)Loads jQuery and the Netcore Smartech App SDK in the required order.
data-smt-action="SMTInAppClose"Allows the SDK to close the message and record the dismissal event.
netcoreTrackClick()Tracks the CTA click and opens the configured deeplink.
Matching heights on both screensBoth screens are capped at the same height on purpose. Lowering the reward screen makes the card visibly resize when the user moves between screens.
Sunburst ray constructionRays are built from CSS borders rather than clip-path or 3D transforms, which render inconsistently in app webviews.

If any of these are missing, the message can still render in the preview pane while failing to close, failing to report, or failing to open your deeplink on a real device.

Update Custom HTML Code for Mystery Box

You can customize the following six sections to configure the Mystery Box widget for your In-app message campaign. All of them live inside the CONFIG block.

Layout: Configure the overall appearance and behavior of the widget.
SettingDescriptionExample
includeDismissButtonShow or hide the close (×) button.true
backgroundBackground color or CSS gradient. Applies to both screens."linear-gradient(180deg, #7B2AA8 0%, #52147A 100%)"
enableConfettiOnWinShow confetti when the reward is revealed.true
revealDelaySecondsPause between the lid popping off and the reward screen appearing.1.1
Sunburst Background: Customize the decorative rays behind the boxes.
SettingDescriptionExample
sunburst.enabledShow or hide the rays entirely.true
sunburst.colorRay color. A low-opacity white reads best against a dark card."rgba(255, 255, 255, 0.07)"
sunburst.rayCountNumber of rays. Values between 8 and 24 look best. Below 4 falls back to 12.12
sunburst.spinSlowly rotate the whole sunburst.true
sunburst.spinSecondsSeconds for one full rotation. Higher values are slower.70
📘

Keep the Sunburst Subtle

The rays sit behind the heading and the boxes. An opaque color makes text hard to read, and a fast rotation turns a background texture into a distraction. The default of 7% white over 70 seconds is deliberately almost imperceptible.

Boxes: Customize the gift boxes the user chooses from.
SettingDescriptionExample
boxes.countNumber of boxes to show. Accepts 2, 3, or 4. Values outside this range are clamped.3
boxes.bodyColorColor of the box body."#E8172F"
boxes.lidColorColor of the lid. Slightly lighter than the body reads best."#F4384E"
boxes.ribbonColorColor of the ribbon and bow."#FFC3D2"
boxes.hintSmall line shown under the boxes before the user picks."Tap a box to open it"
boxes.openingHintReplaces the hint once a box has been tapped."Opening your gift..."
boxes.shakeSecondsHow long the picked box shakes before the lid comes off.0.7
📘

Box Count Is a Layout Choice, Not an Odds Choice

The number of boxes has no relationship to the number of rewards. Three boxes with five rewards is perfectly valid. Four boxes is the practical maximum before the row crowds a 320px card.

Text & Typography: Customize the content shown on both screens.

Pick Screen

SettingDescription
mainTitle.textMain heading. Supports basic HTML like <br>.
mainTitle.fontSizeHeading size.
mainTitle.colorHeading color. Also applied to the reward screen heading.
subtitle.textSupporting text below the heading.
subtitle.fontSizeSubtitle size.
subtitle.colorSubtitle color. Also applied to the reward description.

Reward Screen

SettingDescription
copyInstruction.colorInstruction text color before copying.
copyInstruction.copiedColorColor shown after the coupon is copied.
Buttons: Customize the primary action shown on the reward screen.

Claim Button

SettingDescription
claimButton.enabledShow or hide the Claim button.
claimButton.textButton label.
claimButton.backgroundBackground color or CSS gradient.
claimButton.colorText color.
claimButton.urlDeep link opened after the reward is claimed.
claimButton.payloadOptional payload passed to your app. Use null if not required.
Rewards: Each object in the rewards array is one possible outcome.
FieldDescription
titleHeading displayed on the reward screen.
valueCoupon or reward code copied to the clipboard.
subtitleSupporting message, such as expiry or terms.
probabilityRelative chance of winning this reward.

Probability Workflow

The values are relative weights, not percentages. For example, you have set the rewards section as:

RewardProbability
Free Shipping2
Flat Discount1
Buy 1 Get 11

The first reward is twice as likely to be selected as the other two. Setting a value to zero keeps the reward in the list but makes it impossible to win.

Troubleshoot and FAQs

Q. Why are the boxes cut off or crowded?

A. The boxes.count value may be set to 4 on a narrow card. Reduce it to 3.

Q. Why does the card visibly resize after a box is picked?

A. The reward screen height cap may have been changed. Both screens must share the same maximum height.

Q. Why is the text hard to read over the background?

A. The sunburst.color value may be too opaque. Lower the alpha value, or set sunburst.enabled to false.

Q. Why doesn't anything happen when I tap a box?

A. A reward entry may be malformed, or the rewards array may be empty. Check that every reward has a value and a probability.

Q. Why doesn't anything happen when I click Claim Reward?

A. Verify that claimButton.url has been replaced with a valid deeplink and that the deeplink is registered in your app.

Q. Why does the message render as garbled text?

A. A non-ASCII character may have been pasted into the file. Replace emoji, smart quotes, dashes, and currency symbols with unicode escapes.

Q. Why doesn't the message close when I click the close button?

A. The data-smt-action="SMTInAppClose" attribute may have been removed from the close button. Restore the attribute to enable the SDK to close the message correctly.

Q. Why doesn't the reward code copy on some devices?

A. Some app webviews restrict clipboard access. If copying is unavailable, users can still view and manually enter the reward code.

Q. Why doesn't the template render at all?

A. The SDK block may have been modified or reordered, or the Netcore Smartech SDK may not be integrated into your app correctly. Ensure the SDK block remains unchanged and is loaded in the required order.




Did this page help you?