> ## Documentation Index
> Fetch the complete documentation index at: https://docs.supercycle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# POS overview

> Create and manage cycles from Shopify POS with the calendar, subscription, membership, and resale methods

export const posCreateCyclesScope = () => ({
  PosThumb,
  RenderTablet
});

export const POS_DURATIONS = [{
  label: "4 days",
  price: 63.99
}, {
  label: "7 days",
  price: 83.99
}, {
  label: "14 days",
  price: 117.99
}, {
  label: "30 days",
  price: 161.99
}];

export const POS_METHODS = ["Calendar", "Subscription", "Membership", "Resale"];

export const POS_TILES = [{
  label: "Add custom sale",
  icon: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm-4-9h8m-4-4v8"
}, {
  label: "Apply discount",
  icon: "M12 3l2.2 2.2 3.1-.4.4 3.1L20 10l-2.2 2.2.4 3.1-3.1.4L12 18l-2.2-2.2-3.1.4-.4-3.1L4 10l2.2-2.2-.4-3.1 3.1.4L12 3Zm-2.5 4.5l5 5m-4.5 0h.01m4.49-4.5h.01"
}, {
  label: "Add staff to sale",
  icon: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm0-9a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm-5.5 6a6.5 6.5 0 0 1 11 0"
}, {
  label: "Cycles",
  cycles: true
}, {
  label: "Add tile",
  icon: "M12 5v14M5 12h14",
  plain: true,
  add: true
}];

export const POS_CSS = `
  .scp { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 17px; line-height: 1.25; color: #fff; -webkit-font-smoothing: antialiased; --scr-ripple: rgba(255,255,255,.45); }
  .scp svg { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
  /* The kit centers button text by default; POS controls are left-aligned like the rest of the screen. */
  .scp button { text-align: left; }
  .scp .scp-apply, .scp .scp-close, .scp .scp-rail-reset { text-align: center; }
  .scp-screen { position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; background: #000; }

  /* iPadOS status bar: time and date left, lock state centered, radios and battery right */
  .scp-statusbar { position: relative; display: flex; align-items: center; justify-content: space-between; height: 24px; padding: 0 18px 0 22px; font-size: 13px; font-weight: 600; }
  .scp-status-center { position: absolute; left: 0; right: 0; text-align: center; font-size: 13px; font-weight: 600; pointer-events: none; }
  .scp-status-icons { display: inline-flex; align-items: center; gap: 6px; }
  .scp-status-icons svg { width: 14px; height: 14px; stroke-width: 2; }
  .scp-battery { position: relative; width: 24px; height: 11px; border: 1.5px solid rgba(255,255,255,.9); border-radius: 3.5px; box-sizing: border-box; }
  .scp-battery::after { content: ""; position: absolute; right: -4px; top: 2.5px; width: 2px; height: 3.5px; background: rgba(255,255,255,.9); border-radius: 0 1px 1px 0; }
  .scp-battery-fill { position: absolute; left: 1.5px; top: 1.5px; bottom: 1.5px; width: 40%; background: #fff; border-radius: 1.5px; }

  .scp-pos { display: grid; grid-template-columns: 80px minmax(0, 1fr) 436px; column-gap: 18px; padding: 8px 18px 0 0; flex: 1; min-height: 0; }

  /* Rail: 24pt glyphs on a 63pt pitch; the active item carries a 4pt primary bar on the screen's left edge */
  .scp-rail { display: flex; flex-direction: column; align-items: center; gap: 39px; padding-top: 26px; position: relative; }
  .scp-rail-icon { position: relative; color: #fff; display: grid; place-items: center; width: 24px; height: 24px; }
  .scp-rail-icon svg { width: 24px; height: 24px; }
  .scp-rail-icon.is-active::before { content: ""; position: absolute; left: -40px; top: -6px; width: 4px; height: 36px; border-radius: 0 3px 3px 0; background: #0a6cff; }
  .scp-rail-spacer { flex: 1; }
  .scp-rail-box { background: #1c1c1e; border-radius: 12px; width: 48px; height: 48px; margin-top: -18px; }
  .scp-rail-box svg { width: 22px; height: 22px; }
  /* Reset lives in the rail as a refresh glyph; it wakes up once the example has moved on from its resting state. */
  .scp-rail-reset { border: 0; padding: 0; cursor: pointer; color: rgba(255,255,255,.35); font: inherit; }
  .scp-rail-reset.is-active { color: #fff; }
  .scp-rail-reset:hover { color: #fff; }

  /* Search row and smart grid */
  .scp-main { display: flex; flex-direction: column; gap: 16px; min-width: 0; padding-top: 12px; }
  .scp-searchrow { display: grid; grid-template-columns: minmax(0, 1fr) 48px; gap: 14px; }
  .scp-search { display: flex; align-items: center; gap: 12px; height: 48px; background: #1c1c1e; border-radius: 14px; padding: 0 18px; font-size: 17px; color: #fff; }
  .scp-search svg { width: 20px; height: 20px; }
  .scp-scan { margin-left: auto; color: #fff; }
  .scp-tap { height: 48px; background: #1c1c1e; border-radius: 14px; display: grid; place-items: center; color: #34c759; }
  .scp-tap svg { width: 22px; height: 22px; }
  .scp-tiles { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 17px; align-content: start; }
  .scp-tile { height: 116px; box-sizing: border-box; background: #1c1c1e; border: 2px solid transparent; border-radius: 14px; padding: 12px 14px 14px; display: flex; flex-direction: column; justify-content: space-between; align-items: flex-start; color: #fff; font: inherit; text-align: left; cursor: default; }
  .scp-tile-icon { width: 20px; height: 20px; color: #0a6cff; display: grid; place-items: center; }
  .scp-tile-icon svg { width: 20px; height: 20px; }
  .scp-tile-icon-plain { color: #fff; }
  /* The Supercycle POS app icon: the logomark on the app's pink tile */
  .scp-tile-mark { width: 20px; height: 20px; border-radius: 5px; background: #f9d5d3; display: grid; place-items: center; }
  .scp-tile-mark img { width: 17px; height: 17px; display: block; }
  .scp-tile-text { display: grid; gap: 2px; min-width: 0; justify-items: start; text-align: left; }
  .scp-tile-label { font-size: 17px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .scp-tile-sub { font-size: 15px; color: #8e8e93; }
  .scp-tile-cycles { cursor: pointer; }
  .scp-tile-cycles.is-pressed { transform: scale(.97); }
  .scp-tile-cycles.is-applied { background: #0b1f44; border-color: #0a6cff; color: #4d8dff; }
  .scp-tile-cycles.is-applied .scp-tile-sub { color: #4d8dff; }
  .scp-tile-add { background: transparent; border-color: #3a3a3c; }
  .scp-pager { display: inline-flex; align-items: center; gap: 8px; align-self: center; margin-top: auto; padding: 8px 12px; border-radius: 14px; background: #1c1c1e; color: #fff; font-size: 13px; }
  .scp-pager-dot { width: 6px; height: 6px; border-radius: 50%; background: #fff; }

  /* Cart panel */
  .scp-cart { display: flex; flex-direction: column; background: #1c1c1e; border-radius: 16px; padding: 18px; min-width: 0; margin-bottom: 14px; }
  .scp-cart-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; }
  .scp-cart-title { font-size: 22px; font-weight: 700; }
  .scp-cart-actions { display: inline-flex; gap: 22px; color: #fff; }
  .scp-cart-actions svg { width: 22px; height: 22px; }
  .scp-add-customer { color: #0a6cff; font-size: 17px; font-weight: 600; }
  .scp-required { color: #8e8e93; font-size: 15px; margin-top: 2px; }
  .scp-lines { display: flex; flex-direction: column; gap: 16px; margin-top: 22px; flex: 1; }
  .scp-line { display: flex; align-items: flex-start; gap: 16px; min-width: 0; }
  .scp-thumb { width: 72px; height: 72px; flex-shrink: 0; border-radius: 12px; overflow: hidden; background: #2c2c2e; display: grid; place-items: center; color: #d1d1d6; }
  .scp-thumb svg { width: 30px; height: 30px; }
  .scp-copy { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
  .scp-name { font-size: 17px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .scp-sub { font-size: 15px; color: #a1a1a6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .scp-pill { display: inline-block; align-self: flex-start; margin-top: 2px; font-size: 13px; padding: 3px 8px; border-radius: 6px; background: #3a3a3c; color: #e5e5ea; }
  .scp-line-price { font-size: 17px; white-space: nowrap; padding-top: 2px; }
  .scp-totals { display: flex; flex-direction: column; gap: 6px; padding-top: 14px; }
  .scp-total-line { display: flex; justify-content: space-between; align-items: baseline; font-size: 17px; font-weight: 600; }
  .scp-total-line svg { width: 12px; height: 12px; stroke-width: 2.4; vertical-align: middle; }
  .scp-total-sub { display: flex; justify-content: space-between; font-size: 15px; color: #a1a1a6; }
  .scp-taxes { margin-top: 12px; display: grid; gap: 4px; }
  .scp-taxes-label { font-size: 17px; font-weight: 600; }
  .scp-taxes-value { font-size: 15px; color: #a1a1a6; }
  .scp-checkout { margin-top: 18px; height: 54px; display: grid; place-items: center; background: #0a6cff; color: #fff; border-radius: 10px; font-size: 17px; font-weight: 600; }

  /* Create cycles sheet over the dimmed POS screen, measured from the device */
  .scp-dim { position: absolute; inset: 0; background: rgba(0,0,0,.45); z-index: 4; }
  .scp-modal { position: absolute; top: 45px; left: 242px; width: 710px; height: 740px; box-sizing: border-box; background: #1c1c1e; color: #fff; border-radius: 12px; display: flex; flex-direction: column; overflow: hidden; z-index: 5; box-shadow: 0 0 0 1px rgba(255,255,255,.06), 0 24px 60px rgba(0,0,0,.6); }
  .scp-modal-head { position: relative; display: flex; align-items: center; justify-content: center; height: 50px; border-bottom: 1px solid #2c2c2e; font-size: 17px; font-weight: 600; }
  .scp-close { position: absolute; left: 14px; top: 8px; width: 34px; height: 34px; display: grid; place-items: center; background: none; border: 0; color: #fff; cursor: pointer; border-radius: 8px; }
  .scp-close svg { width: 22px; height: 22px; stroke-width: 2.6; }
  .scp-modal-brand { display: flex; align-items: center; gap: 10px; padding: 12px 14px 0; font-size: 17px; color: #a1a1a6; }
  .scp-modal-brand .scp-tile-mark { width: 24px; height: 24px; border-radius: 50%; }
  .scp-modal-brand .scp-tile-mark img { width: 19px; height: 19px; }
  .scp-modal-body { display: grid; grid-template-columns: 450px 200px; column-gap: 34px; padding: 40px 14px 0 12px; flex: 1; min-height: 0; align-content: start; }
  .scp-modal-items { display: flex; flex-direction: column; gap: 18px; }
  .scp-modal-row { display: flex; justify-content: space-between; align-items: baseline; }
  .scp-modal-label { font-size: 17px; color: #a1a1a6; }
  .scp-modal-label-gap { margin-top: 22px; }
  .scp-link { color: #0a6cff; font-size: 17px; background: none; border: 0; padding: 0; cursor: pointer; font: inherit; }
  .scp-modal-item { display: flex; align-items: center; gap: 16px; background: none; border: 0; padding: 0; color: #fff; font: inherit; text-align: left; cursor: pointer; width: 100%; }
  .scp-checkbox { width: 18px; height: 18px; flex-shrink: 0; border-radius: 4px; border: 1.5px solid #8e8e93; display: grid; place-items: center; color: #1c1c1e; box-sizing: border-box; margin-right: 4px; }
  .scp-checkbox.is-checked { background: #fff; border-color: #fff; }
  .scp-checkbox svg { width: 12px; height: 12px; stroke-width: 2.6; opacity: 0; }
  .scp-checkbox.is-checked svg { opacity: 1; }
  .scp-modal-thumb { width: 50px; height: 50px; flex-shrink: 0; border-radius: 12px; overflow: hidden; background: #2c2c2e; display: grid; place-items: center; color: #d1d1d6; }
  .scp-modal-thumb svg { width: 22px; height: 22px; }
  .scp-modal-name { font-size: 17px; font-weight: 600; }
  .scp-modal-variant { font-size: 15px; color: #a1a1a6; }
  .scp-avail { display: inline-flex; margin-top: 4px; font-size: 13px; }
  .scp-avail-checking { color: #8e8e93; }
  .scp-avail-ok { background: rgba(53,214,118,.16); color: #5fd38a; border-radius: 6px; padding: 3px 9px; font-weight: 600; }
  .scp-apply { margin-top: 8px; height: 48px; background: #2a2a2c; color: #6e6e73; border: 0; border-radius: 10px; text-align: center; font: inherit; font-size: 17px; font-weight: 600; transition: background-color 150ms ease, color 150ms ease; cursor: default; }
  .scp-apply.is-live { background: #0a6cff; color: #fff; cursor: pointer; }
  .scp-modal-form { display: flex; flex-direction: column; gap: 25px; }
  .scp-radio-row { display: inline-flex; align-items: center; gap: 16px; font-size: 17px; color: #fff; white-space: nowrap; background: none; border: 0; padding: 0; font: inherit; cursor: pointer; text-align: left; line-height: 17px; }
  .scp-radio-row[disabled] { color: #6e6e73; cursor: default; }
  .scp-radio { width: 17px; height: 17px; flex-shrink: 0; border-radius: 50%; border: 2px solid #8e8e93; box-sizing: border-box; transition: border-color 150ms ease; }
  .scp-radio-row.is-selected .scp-radio { border: 5px solid #fff; }
  .scp-field { margin-top: -4px; height: 60px; box-sizing: border-box; border: 1px solid #3a3a3c; border-radius: 10px; padding: 0 18px; display: flex; flex-direction: column; justify-content: center; gap: 2px; }
  .scp-field-label { font-size: 13px; color: #8e8e93; }
  .scp-field-value { font-size: 17px; }
  @media (prefers-reduced-motion: reduce) { .scp-apply, .scp-radio { transition: none; } }
`;

export const POS = {
  ground: "#000000",
  surface: "#1c1c1e",
  field: "#2c2c2e",
  outline: "#3a3a3c",
  primary: "#0a6cff",
  text: "#ffffff",
  subdued: "#8e8e93",
  disabledText: "#6e6e73",
  selectedFill: "#0b1f44",
  success: "rgba(53, 214, 118, .16)",
  successText: "#5fd38a",
  width: 1194,
  height: 834
};

export const PosThumb = ({size = 32, className}) => <span className={className} style={{
  width: size,
  height: size
}}>
    <svg viewBox="0 0 24 24" style={{
  width: Math.round(size * 0.42),
  height: Math.round(size * 0.42)
}}>
      <rect x="3" y="5" width="18" height="14" rx="2" />
      <circle cx="8" cy="10" r="1.6" />
      <path d="M4 17l5-4.5 3 2.8 3-2.5 5 4.2" />
    </svg>
  </span>;

export const PosCreateCycles = () => {
  const {PosThumb, RenderTablet} = posCreateCyclesScope();
  const [modal, setModal] = useState(false);
  const [pressed, setPressed] = useState(false);
  const [selected, setSelected] = useState(false);
  const [method, setMethod] = useState(null);
  const [duration, setDuration] = useState(null);
  const [checked, setChecked] = useState(false);
  const [applied, setApplied] = useState(false);
  const money = n => "$" + n.toFixed(2);
  const PLAIN = 549;
  const chosen = duration === null ? null : POS_DURATIONS[duration];
  const linePrice = applied && chosen ? chosen.price : PLAIN;
  const tax = Math.round(linePrice * 10) / 100;
  useEffect(() => {
    if (duration === null || !selected) {
      setChecked(false);
      return undefined;
    }
    setChecked(false);
    const t = setTimeout(() => setChecked(true), 700);
    return () => clearTimeout(t);
  }, [duration, selected]);
  const openModal = () => {
    setPressed(true);
    setTimeout(() => setPressed(false), 200);
    setTimeout(() => setModal(true), 180);
  };
  const apply = () => {
    if (!checked) return;
    setModal(false);
    setApplied(true);
  };
  const reset = () => {
    setModal(false);
    setApplied(false);
    setDuration(null);
    setChecked(false);
    setSelected(false);
    setMethod(null);
  };
  const live = checked && selected && method === 0;
  return <RenderTablet label="Example of creating a cycle on Shopify POS" designWidth={POS.width} designHeight={POS.height}>
      <div className="scp">
        <style>{POS_CSS}</style>
        <div className="scp-screen">
          <div className="scp-statusbar" aria-hidden="true">
            <span>16:03  Fri 11 Sep</span>
            <span className="scp-status-center">Unlocked</span>
            <span className="scp-status-icons">
              <svg viewBox="0 0 24 24"><path d="M5 12.5a10 10 0 0 1 14 0M8 15.5a6 6 0 0 1 8 0M11 18.5h2" /></svg>
              <span style={{
    fontSize: 13,
    fontWeight: 600
  }}>40%</span>
              <span className="scp-battery"><span className="scp-battery-fill" /></span>
            </span>
          </div>

          <div className="scp-pos">
            <div className="scp-rail">
              <span className="scp-rail-icon is-active"><svg viewBox="0 0 24 24"><path d="M4 11.5 12 4.5l8 7V20h-5.5v-5.5h-5V20H4z" /></svg></span>
              <span className="scp-rail-icon"><svg viewBox="0 0 24 24"><path d="M4 13h4l2 3h4l2-3h4M6 5h12l2 8v6H4v-6l2-8Z" /></svg></span>
              <span className="scp-rail-icon"><svg viewBox="0 0 24 24"><path d="M4 12V5h7l9 9-7 7-9-9Zm4-3.5h.01" /></svg></span>
              <span className="scp-rail-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4" /><path d="M4 21a8 8 0 0 1 16 0" /></svg></span>
              <span className="scp-rail-icon"><svg viewBox="0 0 24 24" style={{
    fill: "currentColor",
    stroke: "none"
  }}><circle cx="6" cy="12" r="1.8" /><circle cx="12" cy="12" r="1.8" /><circle cx="18" cy="12" r="1.8" /></svg></span>
              <span className="scp-rail-spacer" />
              <span className="scp-rail-icon scp-rail-box"><svg viewBox="0 0 24 24"><rect x="4" y="9" width="16" height="9" rx="2" /><path d="M8 9V5h8v4M9 21h6" /></svg></span>
              <span className="scp-rail-icon scp-rail-box"><svg viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="9" rx="2" /><path d="M8 11V8a4 4 0 0 1 8 0v3" /></svg></span>
              <button type="button" className={"scp-rail-icon scp-rail-box scp-rail-reset" + (applied || modal || duration !== null || method !== null ? " is-active" : "")} aria-label="Reset the example" onClick={reset}>
                <svg viewBox="0 0 24 24"><path d="M19 12a7 7 0 1 1-2.1-5M19 4v4h-4" /></svg>
              </button>
            </div>

            <div className="scp-main">
              <div className="scp-searchrow" aria-hidden="true">
                <div className="scp-search">
                  <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="6.5" /><path d="m20 20-4.2-4.2" /></svg>
                  <span>Search</span>
                  <svg className="scp-scan" viewBox="0 0 24 24"><path d="M4 8V5h3M17 5h3v3M20 16v3h-3M7 19H4v-3M8 9v6M11 9v6M14 9v6M16 9v6" /></svg>
                </div>
                <span className="scp-tap"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="2.5" /><path d="M7.5 7.5a6.4 6.4 0 0 0 0 9M16.5 7.5a6.4 6.4 0 0 1 0 9M4.5 4.5a10.6 10.6 0 0 0 0 15M19.5 4.5a10.6 10.6 0 0 1 0 15" /></svg></span>
              </div>
              <div className="scp-tiles">
                {POS_TILES.map(t => t.cycles ? <button key={t.label} type="button" className={"scp-tile scp-tile-cycles" + (pressed ? " is-pressed" : "") + (applied ? " is-applied" : "") + (!modal && !applied ? " scr-pressable" : "")} onClick={openModal}>
                      <span className="scp-tile-mark"><img src="/images/logo/logomark-dark.svg" alt="" width="17" height="17" /></span>
                      <span className="scp-tile-text">
                        <span className="scp-tile-label">{t.label}</span>
                        <span className="scp-tile-sub">{applied ? "Methods applied to 1 item" : "1 item available"}</span>
                      </span>
                    </button> : <span key={t.label} className={"scp-tile" + (t.add ? " scp-tile-add" : "")} aria-hidden="true">
                      <span className={"scp-tile-icon" + (t.plain ? " scp-tile-icon-plain" : "")}><svg viewBox="0 0 24 24"><path d={t.icon} /></svg></span>
                      <span className="scp-tile-text"><span className="scp-tile-label">{t.label}</span></span>
                    </span>)}
              </div>
              <span className="scp-pager" aria-hidden="true"><span className="scp-pager-dot" />+</span>
            </div>

            <div className="scp-cart" aria-hidden="true">
              <div className="scp-cart-head">
                <span className="scp-cart-title">Cart</span>
                <span className="scp-cart-actions">
                  <svg viewBox="0 0 24 24" style={{
    fill: "currentColor",
    stroke: "none"
  }}><circle cx="6" cy="12" r="1.8" /><circle cx="12" cy="12" r="1.8" /><circle cx="18" cy="12" r="1.8" /></svg>
                  <svg viewBox="0 0 24 24"><path d="M5 7h14M9 7V5h6v2M7 7l1 13h8l1-13M10 11v6M14 11v6" /></svg>
                </span>
              </div>
              <div>
                <div className="scp-add-customer">Add customer</div>
                <div className="scp-required">Required</div>
              </div>
              <div className="scp-lines">
                {applied && <div className="scp-line">
                    <span className="scp-thumb"><svg viewBox="0 0 24 24"><path d="M20 8.5 12 5 4 8.5v7L12 19l8-3.5z" /><path d="m5.5 12.5 3-1.3v2.5" /></svg></span>
                    <span className="scp-copy">
                      <span className="scp-name">Card authorization (POS)</span>
                      <span className="scp-sub">Saved payment method</span>
                      <span className="scp-pill">Digital item</span>
                    </span>
                    <span className="scp-line-price">$0.50</span>
                  </div>}
                <div className="scp-line">
                  <PosThumb className="scp-thumb" size={72} />
                  <span className="scp-copy">
                    <span className="scp-name">Armchair</span>
                    <span className="scp-sub">{applied && chosen ? "Calendar - " + chosen.label + " - " + money(chosen.price) : "Oak"}</span>
                  </span>
                  <span className="scp-line-price">{money(linePrice)}</span>
                </div>
              </div>
              <div className="scp-totals">
                <div className="scp-total-line">
                  <span>Subtotal  ·  {applied ? "2 items" : "1 item"} <svg viewBox="0 0 24 24"><path d="m6 9 6 6 6-6" /></svg></span>
                  <span>{money(linePrice + (applied ? 0.5 : 0))}</span>
                </div>
                {applied && <div className="scp-total-sub"><span>Recurring subtotal  ·  Every 12 months</span><span>$0.00</span></div>}
                <div className="scp-taxes">
                  <span className="scp-taxes-label">Taxes</span>
                  <span className="scp-taxes-value">{money(tax)} - included in product price</span>
                </div>
              </div>
              <div className="scp-checkout">Checkout {money(linePrice + (applied ? 0.5 : 0))}</div>
            </div>
          </div>

          {modal && <div className="scp-dim" aria-hidden="true" />}
          {modal && <div className="scp-modal" role="dialog" aria-modal="true" aria-label="Create cycles">
              <div className="scp-modal-head">
                <button type="button" className="scp-close" aria-label="Close" onClick={() => setModal(false)}>
                  <svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18" /></svg>
                </button>
                <span>Create cycles</span>
              </div>
              <div className="scp-modal-brand" aria-hidden="true">
                <span className="scp-tile-mark"><img src="/images/logo/logomark-dark.svg" alt="" width="17" height="17" /></span>
                <span>Supercycle POS</span>
              </div>
              <div className="scp-modal-body">
                <div className="scp-modal-items">
                  <div className="scp-modal-row">
                    <span className="scp-modal-label">Select items</span>
                    <button type="button" className="scp-link" onClick={() => setSelected(!selected)}>
                      {selected ? "Deselect all" : "Select all"}
                    </button>
                  </div>
                  <button type="button" className={"scp-modal-item" + (!selected && duration !== null ? " scr-pressable" : "")} aria-pressed={selected} onClick={() => setSelected(!selected)}>
                    <span className={"scp-checkbox" + (selected ? " is-checked" : "")}>
                      <svg viewBox="0 0 24 24"><path d="m5 12 5 5 9-10" /></svg>
                    </span>
                    <PosThumb size={50} className="scp-modal-thumb" />
                    <span className="scp-copy">
                      <span className="scp-modal-name">Armchair</span>
                      <span className="scp-modal-variant">Oak</span>
                      {selected && duration !== null && <span className="scp-avail">
                          {checked ? <span className="scp-avail-ok">3 items available</span> : <span className="scp-avail-checking">Checking availability…</span>}
                        </span>}
                    </span>
                  </button>
                  <button type="button" className={"scp-apply" + (live ? " is-live scr-pressable" : "")} aria-disabled={!live || undefined} onClick={apply}>
                    Apply
                  </button>
                </div>
                <div className="scp-modal-form">
                  <span className="scp-modal-label">Method</span>
                  {POS_METHODS.map((m, i) => <button key={m} type="button" className={"scp-radio-row" + (i === method ? " is-selected" : "") + (method === null && i === 0 ? " scr-pressable" : "")} disabled={i !== 0} onClick={() => setMethod(i)}>
                      <span className="scp-radio" />
                      {m}
                    </button>)}
                  {method === 0 && <>
                      <span className="scp-modal-label scp-modal-label-gap" style={{
    marginTop: 0
  }}>Duration</span>
                      {POS_DURATIONS.map((d, i) => <button key={d.label} type="button" className={"scp-radio-row" + (i === duration ? " is-selected" : "") + (duration === null && i === 1 ? " scr-pressable" : "")} onClick={() => setDuration(i)}>
                          <span className="scp-radio" />
                          {d.label}
                        </button>)}
                      <div className="scp-field" aria-hidden="true">
                        <span className="scp-field-label">Start date</span>
                        <span className="scp-field-value">2026-09-10</span>
                      </div>
                    </>}
                </div>
              </div>
            </div>}
        </div>
      </div>
    </RenderTablet>;
};

export const renderKitScope = () => ({
  RenderStyles
});

export const RENDER_CSS = `
  .scr { width: 100%; min-width: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; }
  .scr button { margin: 0; appearance: none; text-align: center; }
  .scr button:focus-visible { outline: 2px solid #0071e3; outline-offset: 2px; }

  /* Browser window: neutral traffic dots, centered URL, the docs' layered shadow. */
  .scr-window { position: relative; width: 100%; margin: 0 auto; display: flex; flex-direction: column; border-radius: 10px; overflow: hidden; touch-action: pan-y;
    box-shadow: 0 0 0 1px rgba(11,13,24,.06), 0 1px 2px rgba(11,13,24,.024), 0 3px 6px rgba(11,13,24,.03), 0 8px 14px rgba(11,13,24,.035), 0 16px 28px rgba(11,13,24,.04); }
  .scr-titlebar { flex: none; display: flex; align-items: center; gap: 6px; padding: 9px 12px; border-bottom: 1px solid #ebebeb; background: #fff; }
  .scr-dot { width: 9px; height: 9px; border-radius: 50%; background: #e3e3e3; }
  .scr-url { margin: 0 auto; min-width: 0; max-width: calc(100% - 48px); font-size: 11px; color: #85817a; background: #f3f2ee; border-radius: 6px; padding: 3px 10px; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .scr-page { position: relative; overflow: hidden; }
  /* container-name: scr is what polaris-css.jsx's @container queries measure,
     so a phone viewport cannot switch the page to a mobile layout. */
  .scr-zoom { margin: 0 auto; box-sizing: border-box; container-type: inline-size; container-name: scr; }

  /* Reset: quiet text control in the chrome, shown once a render has moved on from its resting state. */
  .scr-reset { font-size: 11px; color: #85817a; background: none; border: 0; cursor: pointer; padding: 2px 4px; font-family: inherit; }
  .scr-reset:hover { color: #1a1a1a; }

  /* Tablet: iPad in landscape, near-black bezel, camera on the left edge, iPadOS status bar. */
  .scr-tablet { position: relative; width: 100%; margin: 0 auto; box-sizing: border-box; padding: 12px 12px 12px 20px; background: #1c1c1e; border-radius: 24px; touch-action: pan-y;
    box-shadow: inset 0 0 0 1px rgba(255,255,255,.1), 0 12px 28px rgba(11,13,24,.18); }
  .scr-camera { position: absolute; left: 7px; top: 50%; width: 5px; height: 5px; transform: translateY(-50%); border-radius: 50%; background: #3a3a3c; box-shadow: inset 0 0 0 1px #111; }
  .scr-screen { position: relative; overflow: hidden; border-radius: 12px; background: #000; }
  .scr-tablet .scr-reset { position: absolute; right: 16px; top: 14px; z-index: 7; color: rgba(255,255,255,.55); }
  .scr-tablet .scr-reset:hover { color: #fff; }

  /* Main buttons the reader can press ripple softly so it's clear what responds. Plain text
     links and decorative controls don't. Paused on hover, off under reduced motion. */
  .scr-pressable { position: relative; }
  .scr-pressable::after { content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; animation: scr-ripple 2.4s ease-out infinite; }
  @keyframes scr-ripple {
    0% { box-shadow: 0 0 0 0 var(--scr-ripple, rgba(0, 113, 227, .45)); }
    60% { box-shadow: 0 0 0 10px transparent; }
    100% { box-shadow: 0 0 0 0 transparent; }
  }
  .scr-pressable:hover::after { animation: none; }
  @media (prefers-reduced-motion: reduce) { .scr-pressable::after { animation: none; } }
`;

export const useRenderZoom = (ref, designWidth) => {
  const [zoom, setZoom] = useState(0.6);
  useEffect(() => {
    const el = ref.current;
    if (!el || typeof ResizeObserver === "undefined") return undefined;
    const fit = () => setZoom(Math.max(0.2, Math.min(1, el.clientWidth / designWidth)));
    fit();
    const ro = new ResizeObserver(fit);
    ro.observe(el);
    return () => ro.disconnect();
  }, [designWidth]);
  return zoom;
};

export const RenderReset = ({onClick, className}) => <button type="button" className={"scr-reset" + (className ? " " + className : "")} onClick={onClick}>
    Reset
  </button>;

export const RenderStyles = () => <style>{RENDER_CSS}</style>;

export const RenderTablet = ({label, designWidth = 728, designHeight = 502, actions, children}) => {
  const {RenderStyles} = renderKitScope();
  const ref = useRef(null);
  const zoom = useRenderZoom(ref, designWidth + 32);
  const body = typeof children === "function" ? children({
    zoom
  }) : children;
  return <div className="not-prose scr" aria-label={label}>
      <RenderStyles />
      <div className="scr-tablet" ref={ref} style={{
    padding: 12 * zoom + "px " + 12 * zoom + "px " + 12 * zoom + "px " + 20 * zoom + "px",
    borderRadius: 24 * zoom
  }}>
        <span className="scr-camera" style={{
    left: 7 * zoom,
    width: 5 * zoom,
    height: 5 * zoom
  }} />
        {actions}
        <div className="scr-screen" style={{
    borderRadius: 12 * zoom
  }}>
          <div className="scr-zoom" style={{
    zoom,
    width: designWidth,
    height: designHeight
  }}>
            {body}
          </div>
        </div>
      </div>
    </div>;
};

Supercycle's Shopify POS integration lets staff create and manage cycles at the counter. From POS you can apply calendar and subscription rentals, sell a membership plan or add items to an existing membership, and process resales. The **Cycles** tile on the POS smart grid opens everything.

<Frame type="glass" caption="Tap Cycles, pick a duration, then Apply to see the rental land in the cart.">
  <PosCreateCycles />
</Frame>

***

## Set up Supercycle in POS

Add the Supercycle tile to your POS smart grid once per store.

<Frame type="glass" caption="Adding the Supercycle tile from POS apps settings">
  <iframe src="https://fast.wistia.net/embed/iframe/iyrmnf7hq9" title="Activate Supercycle in Shopify POS" width="100%" height="290" allow="autoplay; fullscreen" frameborder="0" />
</Frame>

<Steps>
  <Step title="Open POS apps settings">
    In the Shopify admin, open the **Point of Sale** sales channel, select **Settings**, then **POS apps**.
  </Step>

  <Step title="Add the Supercycle tile">
    Select **Supercycle**, then **Add** on the smart grid tile.
  </Step>
</Steps>

***

## Create cycles on POS

<Steps>
  <Step title="Wait for the Cycles tile">
    A **Cycles** tile appears on the smart grid after setup. It can take a few minutes. It stays grayed out until the cart has eligible products, or a customer attached if you're selling a membership plan on its own.
  </Step>

  <Step title="Add products to the cart">
    Add the products you want to apply a method to. Selling a membership plan on its own needs no products, only a customer on the cart.
  </Step>

  <Step title="Tap Cycles">
    Go back to the smart grid and tap **Cycles**.
  </Step>

  <Step title="Select items and a method">
    Tick the products, choose the method (**Calendar**, **Subscription**, **Membership**, or **Resale**), set its options such as duration, start date, or minimum term, and tap **Apply**. The products in the cart are replaced by the cycle lines.

    Under **Membership**, pick **New membership plan** to sell a plan, or pick a membership the customer already holds to spend its credits. Selling a plan adds it as its own line, so it can be the only thing on the order.
  </Step>
</Steps>

***

## Supported methods

### Calendar rentals

Create fixed-duration rentals with a start and end date. You can:

* Select a rental duration, for example 1 day, 1 week, or 1 month
* Choose a start date
* Apply the method to several items at once

### Subscription rentals

Start ongoing subscription rentals with a minimum term. You can:

* Select a minimum term, for example 1, 3, or 6 months
* Choose a start date
* Apply the method to several items at once

### Membership rentals

Sell a membership plan, or add items to a membership the customer already has. You can:

* Sign a customer up to a plan at the counter
* Choose the billing option when a plan offers more than one
* Add items to an existing membership

The cart must have a customer attached. **Membership** stays grayed out until it does.

The plan is charged at checkout and the membership is created once the order is paid, the same as when a customer signs up on your storefront. Their credits are ready straight away, so you can add items to the membership on the same visit or a later one.

A customer with no card on file can still be put on a plan. Recurring charges are still created, so collect them yourself until a card is added. To vault a card for later billing, take payment with a card reader or Tap to Pay.

<Note>
  The **Cycles** screen reads the cart once when it opens. If you attach the customer after opening it, close the screen and tap **Cycles** again for **Membership** to become selectable.
</Note>

### Resale

Sell individual serialized items. You can:

* Sell specific serialized items, whether used, refurbished, or unused
* Sell at the configured resale prices
* Pick the right [pricing option](/documentation/methods/resale/product-setup) when a product has different prices for different item conditions

Resale has fewer configuration options on POS than in the admin.

***

## Considerations

### Choose a condition or option

When a product has more than one [method option](/documentation/manage/products/managing-method-options) that could apply to the items in the cart, POS asks you to pick one before the line can be added. That happens when a resale product is priced by item condition and the scanned item could match more than one pricing option, or when a calendar or subscription product has several options targeted at different conditions for the variant in the cart.

<Steps>
  <Step title="Tick the line">
    After tapping **Cycles** and ticking the product line, POS shows a warning badge next to the line: **Condition required**, or **Option required** for options that aren't condition-based.
  </Step>

  <Step title="Pick a condition or option">
    A chooser appears below the line listing each available option with its price. Tap the one that matches the item.
  </Step>

  <Step title="Change your mind (optional)">
    The chooser stays visible after you pick, so you can switch without unticking the line. The line summary updates to the selected condition and price.
  </Step>
</Steps>

If only one option applies, POS picks it and shows no chooser.

### Verify a customer's ID

If [ID verification](/documentation/configuration/risk/identity-verification) is turned on and a customer needs to verify, staff can start the check from POS without leaving the order. The **Verify ID** action appears on the order details and post-purchase screens when a verification is required.

<Steps>
  <Step title="Tap Verify ID">
    From the order details or post-purchase screen, tap **Verify ID**. POS checks the customer's verification status.
  </Step>

  <Step title="Share the QR code or link">
    Ask the customer to scan the QR code with their phone, or copy the link from **Verification link** and send it to them.
  </Step>

  <Step title="Wait for completion">
    The screen checks the status every few seconds and updates when the customer finishes on their device. If the customer is already verified, POS says no verification is required.
  </Step>
</Steps>

The QR code and link open the same verification page, run by the provider you set under <Icon icon="shopify" iconType="solid" /> **[Risk](https://admin.shopify.com/apps/supercycle/settings/risk)**. Customers don't need to log in to your store to complete it.

### Custom fields

Fill in [rental custom fields](/documentation/configuration/custom-data/custom-fields) as part of the create cycles flow on POS, so in-store bookings capture the same data as cycles created from the admin. The method screen shows your shop's rental custom field definitions below the method options, and the values appear on the cycle like a field set from the admin.

POS shows two field types: single line text and multi line text. Other types, such as dates, booleans, and references, aren't shown. Define text fields under <Icon icon="shopify" iconType="solid" /> **[Custom data](https://admin.shopify.com/apps/supercycle/settings/custom_data)** to capture them at the counter.

### Add-ons

[Add-ons](/documentation/manage/products/add-ons) attached to the selected option are applied on POS the same way as on the online store. When staff apply a method, Supercycle adds any required or optional add-on variants to the cart with the cycle line, including deposits, damage protection, and card authorization. Quantity caps are respected: if an add-on is limited to one per cart, a second line that shares it doesn't add another copy.

### Contracts on draft orders

If [e-signatures](/documentation/configuration/risk/e-signatures) are turned on and **Generate contracts for draft orders** is on under <Icon icon="shopify" iconType="solid" /> **[Risk](https://admin.shopify.com/apps/supercycle/settings/risk)**, POS can collect signatures while the cart is still a draft. On the draft order details screen, staff can see each unsigned contract, share a QR code or signing link for the customer to sign on their phone, or email the signing request to the customer.

The ready-to-sign email isn't sent automatically for draft contracts, so use the POS actions when the customer is ready to sign. When the draft completes as an order, the contracts carry over. See [Contracts on draft orders](/documentation/configuration/risk/e-signatures#contracts-on-draft-orders).

### Several units of one variant

Tapping the same variant twice in POS gives one cart line at quantity 2. Supercycle rents each unit as its own cycle, so the **Cycles** screen shows one row per unit, labeled **Unit 1 of 2**, **Unit 2 of 2**, and so on. Each row has its own selection, method options, and custom fields, so you can set different dates or a different option on each unit, or apply the same setup to all the rows together.

Availability reads against the units. A line with 2 units and 1 item free shows **Only 1 of 2 items available** as a warning rather than a green count. Deselect a unit or pick different dates before you apply.

The method form is shared, so units with different setups are applied in passes:

<Steps>
  <Step title="Apply to the first unit">
    Tick the unit, choose and configure a method, then tap **Apply**. POS replaces the quantity-2 line with a single line for the applied unit. Unticked units stay in the cart as they were.
  </Step>

  <Step title="Apply to the remaining units">
    The **Cycles** screen stays open while a unit is still waiting. Rows that already have a cycle read **Cycle applied** with their dates. Tick the next unit, configure it, and apply again.
  </Step>

  <Step title="Finish">
    The screen closes once every unit has a cycle. Each unit reaches checkout as its own line with its own cycle.
  </Step>
</Steps>

<Note>
  A sale item in the cart that isn't a rental also counts as a waiting row, so the screen can stay open after the last unit is applied. Tap back to return to the cart.
</Note>
