debug / SubscribeWorkflow × Frameworks

push: … caps: …
Pattern: SubscribeWorkflow.create()… is framework-agnostic — it fires DOM events (sub:caps-changed) that every framework can react to naturally. Each section below is an independent island using a different rendering approach for the same cap-gated content pattern.

Shared state bridge: All islands read from window.SubscriptionHub.getCaps() on mount, then stay in sync via document.addEventListener('sub:caps-changed', …).
Alpine.js v3 Directive-based island

x-data + x-init component. Caps flow in via sub:caps-changed → Alpine's reactivity handles all UI updates automatically. Zero boilerplate.

🔓 محتوى حصري — أنت مشترك هذا المحتوى مرئي فقط لمن لديه صلاحية read.
🔒

محتوى حصري — اشترك للوصول

caps:
// Alpine.js integration pattern // Register once — reuse across any x-data island function subscribeIsland(instanceId) { return { caps: [], boot() { // Hydrate from hub state on mount const hub = window.SubscriptionHub; if (hub) this.caps = [...hub.getCaps()]; // Stay in sync with hub via DOM event document.addEventListener('sub:caps-changed', e => { this.caps = [...e.detail.caps]; }); }, hasCap(cap) { return this.caps.includes(cap); }, openWorkflow() { window.SubscribeWorkflow.create() .title('اشترك للوصول') .require('push', { label: '🔔 اشترك مجاناً', grants: ['read', 'book-download'], }) .optional('email', { emailMode: 'form' }) .// onComplete: not needed — Alpine reacts to caps-changed showModal(); }, }; } // HTML (no JS in template): // <div x-data="subscribeIsland()" x-init="boot()"> // <div x-show="hasCap('read')"> premium content </div> // <button x-show="!hasCap('read')" @click="openWorkflow()"> // Subscribe</button> // </div>
@preact/signals-core Signal primitives

No component tree — just signal() + effect(). Effects write directly to raw DOM nodes. Fine-grained: only nodes that read a changed signal re-run.

🔓 محتوى حصري — أنت مشترك الصلاحية read نشطة — المحتوى مفتوح.
🔒

محتوى حصري — اشترك للوصول

caps: none
// @preact/signals-core — no component tree needed // Works in any JS context: vanilla, Alpine, Lit, etc. import { signal, computed, effect } from '@preact/signals-core'; // ── State ───────────────────────────────── const caps = signal([]); const hasRead = computed(() => caps.value.includes('read')); // ── Sync from hub ────────────────────────── const hub = window.SubscriptionHub; if (hub) caps.value = [...hub.getCaps()]; document.addEventListener('sub:caps-changed', e => { caps.value = [...e.detail.caps]; // triggers all effects }); // ── Effects: update DOM when signals change ─ effect(() => { gate.style.display = hasRead.value ? 'none' : 'block'; content.style.display = hasRead.value ? 'block' : 'none'; }); effect(() => { capsList.innerHTML = caps.value.length ? caps.value.map(c => `<span class="cap-chip active">${c}</span>` ).join('') : '<span style="color:var(--muted)">none</span>'; }); // ── Workflow trigger ─────────────────────── btn.addEventListener('click', () => { window.SubscribeWorkflow.create() .require('push', { grants: ['read', 'notifications'], label: '🔔 اشترك مجاناً', }) .optional('email', { emailMode: 'form' }) .showModal(); // No onComplete needed — caps signal updates // automatically via sub:caps-changed });
Lit v3 Web component

<subscribe-cta-lit> — drop it anywhere, no framework needed at the call site. Uses light DOM (no shadow root) so GateCard styles bleed in naturally. <slot> for slotted content.

محتوى مقفل — اشترك للوصول

🔓 مرحباً! أنت مشترك هذا المحتوى مرئي فقط لمن لديه صلاحية read.
// Lit v3 — LitElement web component // Light DOM (no shadow root) so page styles apply normally import { LitElement, html } from 'lit'; class SubscribeCtaLit extends LitElement { // Reflect attributes → reactive properties static properties = { caps: { type: Array }, workflowTitle: { type: String, attribute: 'workflow-title' }, workflowDesc: { type: String, attribute: 'workflow-desc' }, btnLabel: { type: String, attribute: 'btn-label' }, requiredCap: { type: String, attribute: 'required-cap' }, }; // Light DOM: GateCard styles apply, slots work naturally createRenderRoot() { return this; } constructor() { super(); this.caps = []; this.requiredCap = 'read'; } connectedCallback() { super.connectedCallback(); // Hydrate on mount const hub = window.SubscriptionHub; if (hub) this.caps = [...hub.getCaps()]; // Keep in sync this._onCaps = e => { this.caps = [...e.detail.caps]; }; document.addEventListener('sub:caps-changed', this._onCaps); } disconnectedCallback() { super.disconnectedCallback(); document.removeEventListener('sub:caps-changed', this._onCaps); } get _unlocked() { return this.caps.includes(this.requiredCap); } _open() { window.SubscribeWorkflow.create() .title(this.workflowTitle || 'اشترك') .desc(this.workflowDesc || '') .require('push', { label: this.btnLabel || '🔔 اشترك مجاناً', grants: ['read', 'book-download'], }) .optional('email', { emailMode: 'form' }) .showModal(); // Lit re-renders when caps prop changes via sub:caps-changed } render() { return html` <div class="demo-box"> ${this._unlocked ? html`<slot name="unlocked"></slot>` : html` <slot name="locked"></slot> <button class="demo-btn" @click=${this._open}> ${this.btnLabel || '🔔 اشترك'} </button>` } <div class="caps-row"> <span>caps:</span> ${this.caps.map(c => html`<span class="cap-chip active">${c}</span>` )} ${!this.caps.length ? html`<span style="color:var(--muted)">none</span>` : ''} </div> </div> `; } } customElements.define('subscribe-cta-lit', SubscribeCtaLit); // Usage — just drop the element anywhere: // <subscribe-cta-lit // workflow-title="Get free books" // btn-label="🔔 Subscribe free" // required-cap="read"> // <div slot="unlocked">Premium content here</div> // </subscribe-cta-lit>
▲ event log 0 events