Meeteor Ticket Widget

Embed a fully functional ticket purchasing experience into any website or app in minutes.

What you get

A complete checkout flow — ticket selection, promo codes, sponsor videos, contact form, Stripe payment, and confirmation — all embedded inside your product. Supports dark mode, 4 layout modes, buyer prefill for instant checkout, and real-time webhooks.

Quick Start

The fastest way to embed tickets. Paste this into any HTML page:

1

Create a widget

In the Meeteor app, go to your event → Embed Widget tab → Configure and save. Copy the widgetId.

2

Add the embed code

Paste into your HTML:

<div data-meeteor-widget data-widget-id="YOUR_WIDGET_ID"></div>
<script src="https://widget.meeteor.me/embed.js" async></script>

That's it. The widget loads, auto-resizes, and handles the full purchase flow.

JavaScript Embed

The recommended approach for any website. The embed script creates an iframe, manages auto-resizing, and communicates via postMessage.

HTML
<div
  data-meeteor-widget
  data-widget-id="YOUR_WIDGET_ID"
  data-buyer-name="Jane Smith"
  data-buyer-email="jane@example.com"
  data-buyer-phone="+15551234567"
></div>
<script src="https://widget.meeteor.me/embed.js" async></script>

Data Attributes

AttributeRequiredDescription
data-widget-idYesYour widget ID from the Meeteor dashboard
data-buyer-nameNoPre-fill buyer's full name. If valid name + email provided, contact step is skipped.
data-buyer-emailNoPre-fill buyer's email address
data-buyer-phoneNoPre-fill buyer's phone number
data-support-registrationNoSet to true to enable post-purchase registration modal when the sale returns shouldRegister: true

iframe Embed

For full manual control. Set any query params directly on the URL.

HTML
<iframe
  src="https://widget.meeteor.me/w/YOUR_WIDGET_ID?theme=dark&name=Jane+Smith&email=jane@example.com"
  width="100%"
  height="600"
  frameborder="0"
  allow="payment"
  style="border:none;border-radius:12px;max-width:480px"
></iframe>

URL Parameters

ParameterDescription
themedark or light — force color scheme
nameBuyer's full name (first + last)
emailBuyer's email address
phoneBuyer's phone number
supportRegistrationtrue — enable post-purchase registration modal when sale returns shouldRegister: true

WordPress

Use the Meeteor Ticket Widget plugin to embed tickets on any WordPress page or post.

1

Install the plugin

Upload and activate the plugin from wordpress-plugin/meeteor-ticket-widget/ (zip the folder first, then Plugins → Add New → Upload Plugin).

2

Configure (optional)

Go to Settings → Meeteor to set a default Widget ID. This lets you use [meeteor_widget] without specifying an ID.

3

Add the shortcode

Insert the shortcode in any page or post:

Shortcode
[meeteor_widget widget_id="YOUR_WIDGET_ID"]

Or, if you set a default Widget ID in Settings:

[meeteor_widget]

Shortcode Attributes

AttributeDefaultDescription
widget_id(from Settings)Your Meeteor widget ID. Required if no default is set.
modescriptscript = JS embed (auto-resize), iframe = fixed-height iframe
height600Height in pixels (used when mode="iframe")
support_registrationfalseSet to true to enable post-purchase registration modal

Block Editor (Gutenberg): Add a Shortcode block and paste [meeteor_widget widget_id="YOUR_WIDGET_ID"].

React

1

Install the package

npm install @meeteor/ticket-widget
2

Use the component

React / TSX
import { MeeteorTicketWidget } from '@meeteor/ticket-widget';

function TicketPage() {
  return (
    <MeeteorTicketWidget
      widgetId="YOUR_WIDGET_ID"
      buyer={{
        name: "Jane Smith",
        email: "jane@example.com",
        phone: "+15551234567",
      }}
      onPurchaseComplete={(data) => {
        console.log('Sale:', data.sale_id);
        console.log('Tickets:', data.tickets);
      }}
      onError={(err) => console.error(err.code, err.message)}
      onClose={() => console.log('Widget closed')}
      maxWidth={480}
    />
  );
}

Dynamic Updates

React
import { setPromoCode, setBuyerContact } from '@meeteor/ticket-widget';

// Apply a promo code programmatically
setPromoCode(iframeElement, 'SAVE20');

// Update buyer contact at runtime
setBuyerContact(iframeElement, {
  name: 'John Doe',
  email: 'john@example.com',
});

React Native

1

Install

npm install @meeteor/ticket-widget-native react-native-webview
2

Use the component

React Native / TSX
import { MeeteorTicketWidget } from '@meeteor/ticket-widget-native';

export default function TicketScreen() {
  return (
    <MeeteorTicketWidget
      widgetId="YOUR_WIDGET_ID"
      buyer={{
        name: "Jane Smith",
        email: "jane@example.com",
      }}
      onPurchaseComplete={(data) => {
        console.log('Sale:', data.sale_id);
      }}
      onError={(err) => Alert.alert('Error', err.message)}
      onClose={() => navigation.goBack()}
      style={{ flex: 1 }}
    />
  );
}

Dynamic Updates via Ref

React Native
import { setPromoCode, setBuyerContact } from '@meeteor/ticket-widget-native';

const webViewRef = useRef(null);

// Apply promo code
setPromoCode(webViewRef, 'EARLYBIRD');

// Update buyer info
setBuyerContact(webViewRef, { name: 'John Doe', email: 'john@example.com' });

Swift (iOS)

Native iOS package using WKWebView. Distributed as a Swift Package via Git.

Requirements: iOS 14+, Xcode 14+, Swift 5.7+
1

Add the Swift Package in Xcode

Open your Xcode project, then:

  1. Go to File → Add Package Dependencies
  2. In the search bar, paste the package URL:
https://gitlab.com/vic2tee4u/meeteor-widget-ios.git
  1. Set Dependency Rule to "Up to Next Major Version" starting from 0.1.0
  2. Click Add Package
  3. In the dialog, check MeeteorWidget and select your app target → click Add Package

Verify installation

In any Swift file, add import MeeteorWidget. If it compiles without errors, the package is installed correctly.

2

Add to your Info.plist (if needed)

The widget loads web content, so ensure your app allows arbitrary loads or add the widget domain:

Info.plist
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoadsInWebContent</key>
    <true/>
</dict>
3

Use in SwiftUI

Swift (SwiftUI)
import SwiftUI
import MeeteorWidget

struct TicketView: View {
    @Environment(\.dismiss) var dismiss

    var body: some View {
        MeeteorTicketWidget(
            widgetId: "YOUR_WIDGET_ID",
            buyer: BuyerInfo(
                name: "Jane Smith",
                email: "jane@example.com",
                phone: "+15551234567"
            ),
            onPurchaseComplete: { data in
                print("Sale completed: \(data.saleId)")
                print("Tickets: \(data.tickets)")
                // Navigate to confirmation, update UI, etc.
            },
            onError: { error in
                print("Error \(error.code): \(error.message)")
            },
            onClose: {
                dismiss()
            }
        )
        .edgesIgnoringSafeArea(.bottom)
    }
}

Present as a sheet

Swift (SwiftUI)
struct EventDetailView: View {
    @State private var showTickets = false

    var body: some View {
        Button("Buy Tickets") {
            showTickets = true
        }
        .sheet(isPresented: $showTickets) {
            NavigationStack {
                MeeteorTicketWidget(
                    widgetId: "YOUR_WIDGET_ID",
                    buyer: BuyerInfo(name: "Jane Smith", email: "jane@example.com"),
                    onPurchaseComplete: { _ in showTickets = false }
                )
                .navigationTitle("Tickets")
                .navigationBarTitleDisplayMode(.inline)
                .toolbar {
                    ToolbarItem(placement: .cancellationAction) {
                        Button("Close") { showTickets = false }
                    }
                }
            }
        }
    }
}
4

Use in UIKit

Swift (UIKit)
import UIKit
import MeeteorWidget

class TicketViewController: UIViewController {
    private let widget = MeeteorTicketWidgetView()

    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Tickets"

        // Add widget to view
        view.addSubview(widget)
        widget.frame = view.bounds
        widget.autoresizingMask = [.flexibleWidth, .flexibleHeight]

        // Set callbacks
        widget.onPurchaseComplete = { [weak self] data in
            print("Sale: \(data.saleId)")
            self?.navigationController?.popViewController(animated: true)
        }
        widget.onError = { error in
            print("Error: \(error.message)")
        }
        widget.onClose = { [weak self] in
            self?.dismiss(animated: true)
        }

        // Load the widget
        widget.load(
            widgetId: "YOUR_WIDGET_ID",
            buyer: BuyerInfo(name: "Jane Smith", email: "jane@example.com")
        )
    }
}
5

Dynamic updates at runtime

After the widget is loaded, you can update buyer info or apply promo codes programmatically:

Swift
// UIKit — via the widget view
widget.setPromoCode("SAVE20")
widget.setBuyerContact(BuyerInfo(name: "John Doe", email: "john@example.com"))

// SwiftUI — via the coordinator (access through the UIViewRepresentable)
// Typically you'd pass updated buyer info as a prop and the widget URL updates automatically
Tip: If you pass a valid full name (first + last) and email via BuyerInfo, the contact form step is automatically skipped — users go straight from ticket selection to payment.

Kotlin (Android)

Native Android library using WebView. Supports both Jetpack Compose and XML Views.

1

Add the dependency

Add the module to your build.gradle.kts:

implementation("me.meeteor:widget:1.0.0")
2

Jetpack Compose

Kotlin (Compose)
import me.meeteor.widget.*

@Composable
fun TicketScreen() {
    MeeteorTicketWidget(
        widgetId = "YOUR_WIDGET_ID",
        buyer = BuyerInfo(
            name = "Jane Smith",
            email = "jane@example.com",
            phone = "+15551234567"
        ),
        onPurchaseComplete = { data ->
            println("Sale: ${data.saleId}")
        },
        onError = { error ->
            println("Error: ${error.message}")
        },
        onClose = { finish() },
        modifier = Modifier.fillMaxSize()
    )
}
3

XML / View System

Kotlin
import me.meeteor.widget.*

class TicketActivity : AppCompatActivity() {
    private lateinit var widget: MeeteorTicketWidget

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        widget = MeeteorTicketWidget(this)
        setContentView(widget)

        widget.listener = object : MeeteorWidgetListener {
            override fun onPurchaseComplete(data: PurchaseData) {
                println("Sale: ${data.saleId}")
            }
            override fun onError(error: WidgetError) {
                Toast.makeText(this@TicketActivity, error.message, Toast.LENGTH_SHORT).show()
            }
            override fun onClose() { finish() }
        }

        widget.load(
            widgetId = "YOUR_WIDGET_ID",
            buyer = BuyerInfo(name = "Jane Smith", email = "jane@example.com")
        )
    }

    // Dynamic updates
    fun applyPromo() = widget.setPromoCode("SAVE20")
    fun updateBuyer() = widget.setBuyerContact(
        BuyerInfo(name = "John Doe", email = "john@example.com")
    )
}

Buyer Prefill

Pre-fill the buyer's name, email, and phone to streamline checkout. When a valid full name (first + last) and email are provided, the contact form step is automatically skipped — the user goes directly from ticket selection to payment.

Methods

MethodWhen to Use
URL params (?name=...&email=...)iframe and direct links
Data attributes (data-buyer-name)JavaScript embed
buyer propReact, React Native, Swift, Kotlin
meeteor:set_contact postMessageDynamic updates after load
Auto-skip logic: The contact step is skipped when name contains a space (first + last) AND email is a valid email address. Phone is always optional. Users can still edit via the back button.

Post-Purchase Registration

When enabled, a modal appears after purchase prompting the buyer to create an account and claim points. Enable via:

The modal only appears when both conditions are met: registration support is enabled AND the sale/free-ticket API response includes shouldRegister: true. It collects first name, last name, password, and confirm password (email pre-filled from the ticket). On success, shows a completion state with an option to download the Meeteor app.

Dark Mode

The widget supports automatic and manual dark mode:

MethodExample
URL parameter?theme=dark or ?theme=light
System preferenceAuto-detected via prefers-color-scheme
Theme colorsIf background_color luminance < 0.2, dark mode activates automatically
postMessageSend meeteor:set_theme with { background_color: '#0f0f1a', text_color: '#e8e8f0' }

In dark mode, the widget adjusts card backgrounds, input fields, borders, and muted text for optimal contrast.

Layouts

Choose a layout mode in the widget configuration. Each mode adjusts sizing, image display, and spacing.

LayoutMax WidthImageBest For
vertical480pxTop, 16:9Standard embed (default)
horizontal720pxLeft, 40% widthWide containers, sidebars with space
compact380pxHiddenTight sidebars, popups
full560pxTop, 2:1Dedicated ticket pages

Theme Options

Configure these in the Meeteor app or override via meeteor:set_theme postMessage:

PropertyTypeDefaultDescription
primary_colorhex#6C63FFPrimary brand color (buttons, accents)
secondary_colorhex#FFFFFFSecondary color
background_colorhex#FFFFFFWidget background. Dark values auto-enable dark mode.
text_colorhex#1A1A2EPrimary text color
font_familystringInterCSS font-family name. Any Google Font is loaded automatically.
font_urlstring | nullnullURL to a custom font CSS stylesheet (for non-Google fonts). When set, this is loaded instead of Google Fonts.
border_radiusnumber12Corner radius in px
button_stylestringfilledfilled, outlined, or gradient
Custom fonts: Set font_family to any Google Font name (e.g. "Poppins", "Playfair Display") and it loads automatically. For self-hosted or non-Google fonts, provide a font_url pointing to a CSS file with your @font-face declarations, and set font_family to the matching font name.

postMessage API

Communicate with the widget iframe at runtime using window.postMessage.

Send to Widget (Parent → Widget)

Message TypePayloadDescription
meeteor:set_promo{ code: "SAVE20" }Apply a promo/discount code
meeteor:set_theme{ theme: { primary_color: "#ff0000" } }Override theme properties at runtime
meeteor:set_contact{ contact: { name, email, phone } }Set/update buyer contact info
JavaScript
const iframe = document.querySelector('iframe');

// Apply promo code
iframe.contentWindow.postMessage({
  type: 'meeteor:set_promo',
  code: 'EARLYBIRD'
}, '*');

// Set buyer contact (auto-skips contact step if valid)
iframe.contentWindow.postMessage({
  type: 'meeteor:set_contact',
  contact: { name: 'Jane Smith', email: 'jane@example.com' }
}, '*');

// Override theme
iframe.contentWindow.postMessage({
  type: 'meeteor:set_theme',
  theme: { primary_color: '#ff6600', background_color: '#0f0f1a' }
}, '*');

Callbacks & Events

The widget emits events via postMessage (Widget → Parent). Listen for these in your parent page:

Receive from Widget (Widget → Parent)

EventPayloadDescription
meeteor:ready{ height }Widget loaded and ready. Includes initial height for iframe sizing.
meeteor:resize{ height }Content height changed. Update iframe height to this value.
meeteor:purchase_complete{ data: { sale_id, tickets } }Purchase was successful.
meeteor:error{ error: { code, message } }An error occurred (payment failed, load error, etc.)
meeteor:closeUser requested to close the widget.
JavaScript
window.addEventListener('message', (event) => {
  if (!event.data?.type?.startsWith('meeteor:')) return;

  switch (event.data.type) {
    case 'meeteor:purchase_complete':
      console.log('Sale ID:', event.data.data.sale_id);
      console.log('Tickets:', event.data.data.tickets);
      // Redirect to thank-you page, show confirmation, etc.
      break;

    case 'meeteor:error':
      console.error(event.data.error.code, event.data.error.message);
      break;

    case 'meeteor:resize':
      document.querySelector('iframe').style.height = event.data.height + 'px';
      break;

    case 'meeteor:close':
      // Hide widget modal, etc.
      break;
  }
});

Webhooks

Receive real-time notifications on your server when events happen in the widget. Configure your webhook URL in the Meeteor app under Embed Widget → Configure → Advanced → Webhook URL.

How it works

When a purchase is completed (or other events occur), Meeteor sends an HTTP POST request to your webhook URL with a JSON payload. Your server should respond with a 2xx status code within 10 seconds. Failed deliveries are retried up to 3 times with exponential backoff.

Webhook Events

EventTrigger
purchase.completedA ticket purchase was successfully completed via the widget.
purchase.failedA payment attempt failed (card declined, etc.).
widget.viewA user viewed / loaded the widget.
sponsor.view_completedA user watched a full sponsor video.
discount.appliedA promo code or sponsor discount was applied.

Webhook Verification

Every webhook request includes a signature header so you can verify it came from Meeteor. Your webhook secret is shown in the Embed Code tab after creating a widget.

Headers

HeaderDescription
X-Meeteor-SignatureHMAC-SHA256 hex digest of the raw request body, signed with your webhook secret.
X-Meeteor-EventThe event type (e.g. purchase.completed).
X-Meeteor-DeliveryUnique delivery ID for idempotency.
X-Meeteor-TimestampUnix timestamp (seconds) of when the event was sent.

Verification Example

const crypto = require('crypto');

function verifyWebhook(req, secret) {
  const signature = req.headers['x-meeteor-signature'];
  const timestamp = req.headers['x-meeteor-timestamp'];
  const body = JSON.stringify(req.body);

  // Prevent replay attacks (reject if older than 5 minutes)
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (age > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${body}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}
import hmac, hashlib, time, json

def verify_webhook(headers, body, secret):
    signature = headers.get('X-Meeteor-Signature', '')
    timestamp = headers.get('X-Meeteor-Timestamp', '0')

    # Prevent replay attacks
    if abs(time.time() - int(timestamp)) > 300:
        return False

    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.{json.dumps(body)}".encode(),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)
<?php
function verifyWebhook($headers, $body, $secret) {
    $signature = $headers['X-Meeteor-Signature'] ?? '';
    $timestamp = $headers['X-Meeteor-Timestamp'] ?? '0';

    // Prevent replay attacks
    if (abs(time() - intval($timestamp)) > 300) return false;

    $expected = hash_hmac('sha256', "$timestamp.$body", $secret);
    return hash_equals($expected, $signature);
}

Webhook Payload Reference

purchase.completed

Sent when a ticket purchase is fully processed.

JSON Payload
{
  "event": "purchase.completed",
  "delivery_id": "dlv_abc123def456",
  "timestamp": 1708099200,
  "data": {
    "sale_id": "sale_789xyz",
    "payment_intent_id": "pi_3ABC123",
    "widget_id": "68f3a115-4afc-47ff-8d0a-23b6eb4e4d9e",
    "event_id": "6995fcf976cfb725c1abae9e",
    "buyer": {
      "name": "Jane Smith",
      "email": "jane@example.com",
      "phone": "+15551234567"
    },
    "currency": "usd",
    "subtotal": 3500,
    "fees": 436,
    "total": 3936,
    "items": [
      {
        "tier_id": "6995fcfa76cfb725c1abaead",
        "tier_name": "General Admission",
        "quantity": 1,
        "unit_price": 3500,
        "currency": "usd"
      }
    ],
    "tickets": [
      {
        "ticket_id": "tkt_abc123",
        "tier_name": "General Admission",
        "holder_name": "Jane Smith",
        "holder_email": "jane@example.com",
        "qr_code_url": "https://api.meeteor.me/tickets/tkt_abc123/qr"
      }
    ],
    "discount": {
      "code": "EARLYBIRD",
      "type": "percentage",
      "value": 10
    },
    "fee_breakdown": {
      "platform_fee": 105,
      "platform_fixed_fee": 200,
      "stripe_fee": 101,
      "stripe_fixed_fee": 30,
      "tax": 0
    }
  }
}
Amounts are in cents. subtotal: 3500 = $35.00. Always divide by 100 for display.

purchase.failed

JSON Payload
{
  "event": "purchase.failed",
  "delivery_id": "dlv_def789ghi012",
  "timestamp": 1708099300,
  "data": {
    "widget_id": "68f3a115-4afc-47ff-8d0a-23b6eb4e4d9e",
    "event_id": "6995fcf976cfb725c1abae9e",
    "buyer_email": "jane@example.com",
    "error_code": "card_declined",
    "error_message": "Your card was declined."
  }
}

widget.view

JSON Payload
{
  "event": "widget.view",
  "delivery_id": "dlv_viewabc123",
  "timestamp": 1708099100,
  "data": {
    "widget_id": "68f3a115-4afc-47ff-8d0a-23b6eb4e4d9e",
    "event_id": "6995fcf976cfb725c1abae9e",
    "referrer": "https://your-site.com/events/concert",
    "user_agent": "Mozilla/5.0 ..."
  }
}

sponsor.view_completed

JSON Payload
{
  "event": "sponsor.view_completed",
  "delivery_id": "dlv_spon456",
  "timestamp": 1708099250,
  "data": {
    "widget_id": "68f3a115-4afc-47ff-8d0a-23b6eb4e4d9e",
    "event_id": "6995fcf976cfb725c1abae9e",
    "sponsor_id": "sp_abc123",
    "sponsor_name": "Acme Corp",
    "duration_watched": 30.5,
    "discount_applied": {
      "type": "percentage",
      "value": 5,
      "tier_name": "General Admission"
    }
  }
}

discount.applied

JSON Payload
{
  "event": "discount.applied",
  "delivery_id": "dlv_disc789",
  "timestamp": 1708099280,
  "data": {
    "widget_id": "68f3a115-4afc-47ff-8d0a-23b6eb4e4d9e",
    "event_id": "6995fcf976cfb725c1abae9e",
    "discount_code": "EARLYBIRD",
    "discount_type": "percentage",
    "discount_value": 10,
    "source": "promo"
  }
}
Tip: Use the delivery_id for idempotency. Store processed delivery IDs and skip duplicates to prevent double-processing.

Sample Projects

We provide ready-to-run example projects for every major platform. Each example shows a single "Get Tickets" button that opens the widget in a WebView. Clone the repo and follow the instructions below to try them locally.

Source code: All examples live in the widget/examples/ directory of the repository — View on GitHub. Each project contains a single "Get Tickets" button that opens the widget (ID: 68f3a115-4afc-47ff-8d0a-23b6eb4e4d9e) targeting the dev environment.

React (Vite + TypeScript)

A minimal React app that uses the @meeteor/ticket-widget package. Shows a "Get Tickets" button that renders the widget inline with purchase and error callbacks.

# 1. Navigate to the example
cd widget/examples/react

# 2. Install dependencies
npm install

# 3. Start the dev server
npm run dev

# Opens at http://localhost:5173

Requirements: Node.js 18+.

import { useState, useCallback } from 'react';
import { MeeteorTicketWidget } from '@meeteor/ticket-widget';
import type { PurchaseData } from '@meeteor/ticket-widget';

const WIDGET_ID = '68f3a115-4afc-47ff-8d0a-23b6eb4e4d9e';

export default function App() {
  const [showWidget, setShowWidget] = useState(false);
  const [purchase, setPurchase] = useState<PurchaseData | null>(null);

  const handlePurchaseComplete = useCallback((data: PurchaseData) => {
    setPurchase(data);
    setShowWidget(false);
  }, []);

  return showWidget ? (
    <MeeteorTicketWidget
      widgetId={WIDGET_ID}
      onPurchaseComplete={handlePurchaseComplete}
      onError={(err) => console.error(err)}
      onClose={() => setShowWidget(false)}
      maxWidth={480}
    />
  ) : (
    <button onClick={() => setShowWidget(true)}>Get Tickets</button>
  );
}
// See the full source in widget/examples/react/

React Native (Expo)

A minimal Expo app with a WebView modal that loads the widget. Handles postMessage events for purchase completion and close.

# 1. Navigate to the example
cd widget/examples/react-native

# 2. Install dependencies
npm install

# 3. Run on iOS simulator or Android emulator
npx expo start --ios
# or
npx expo start --android

Requirements: Node.js 18+, Expo CLI, Xcode (for iOS) or Android Studio (for Android).

import React, { useState, useRef } from 'react';
import { View, Text, TouchableOpacity, Modal, SafeAreaView } from 'react-native';
import { WebView } from 'react-native-webview';

const WIDGET_URL = 'https://widget-dev.meeteor.me/w/68f3a115-4afc-47ff-8d0a-23b6eb4e4d9e';

export default function App() {
  const [visible, setVisible] = useState(false);

  const handleMessage = (event) => {
    const data = JSON.parse(event.nativeEvent.data);
    if (data.type === 'meeteor:purchase_complete') setVisible(false);
    if (data.type === 'meeteor:widget_close') setVisible(false);
  };

  return (
    <SafeAreaView style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <TouchableOpacity onPress={() => setVisible(true)}>
        <Text>Get Tickets</Text>
      </TouchableOpacity>

      <Modal visible={visible} animationType="slide">
        <WebView source={{ uri: WIDGET_URL }} onMessage={handleMessage} />
      </Modal>
    </SafeAreaView>
  );
}

iOS (SwiftUI)

A SwiftUI app that presents the widget in a .sheet using WKWebView. Includes a JavaScript bridge to receive meeteor:* events.

# 1. Open the Xcode project
open widget/examples/ios/MeeteorExample.xcodeproj

# 2. Select a simulator (e.g. iPhone 16)

# 3. Run (Cmd+R)

Requirements: Xcode 15+, iOS 15+ deployment target.

struct ContentView: View {
    @State private var showWidget = false

    var body: some View {
        Button("Get Tickets") { showWidget = true }
            .sheet(isPresented: $showWidget) {
                NavigationView {
                    WidgetWebView(
                        url: URL(string: "https://widget-dev.meeteor.me/w/68f3a115-4afc-47ff-8d0a-23b6eb4e4d9e")!,
                        onPurchaseComplete: { _ in showWidget = false },
                        onClose: { showWidget = false }
                    )
                    .toolbar {
                        ToolbarItem(placement: .navigationBarTrailing) {
                            Button("Done") { showWidget = false }
                        }
                    }
                }
            }
    }
}

// WidgetWebView uses WKWebView + a JS bridge to receive meeteor:* events.
// See the full source in widget/examples/ios/

Android (Kotlin / Jetpack Compose)

A Compose-based Activity that shows a WebView with the widget. Uses a JavascriptInterface bridge to receive purchase and close events.

# 1. Open the example in Android Studio
#    File → Open → select widget/examples/android/

# 2. Sync Gradle (Android Studio will prompt you)

# 3. Run on emulator or device (Shift+F10)

# Alternatively, build from CLI:
cd widget/examples/android
./gradlew :app:installDebug

Requirements: Android Studio Hedgehog+, SDK 34, Kotlin 1.9+.

@Composable
fun WidgetExample() {
    var showWidget by remember { mutableStateOf(false) }

    if (showWidget) {
        WidgetScreen(onClose = { showWidget = false })
    } else {
        Button(onClick = { showWidget = true }) {
            Text("Get Tickets")
        }
    }
}

@Composable
fun WidgetScreen(onClose: () -> Unit) {
    AndroidView(factory = { context ->
        WebView(context).apply {
            settings.javaScriptEnabled = true
            settings.domStorageEnabled = true
            addJavascriptInterface(object {
                @JavascriptInterface
                fun onMessage(json: String) {
                    if (json.contains("meeteor:purchase_complete") ||
                        json.contains("meeteor:widget_close")) {
                        (context as? ComponentActivity)?.runOnUiThread { onClose() }
                    }
                }
            }, "MeeteorBridge")
            loadUrl("https://widget-dev.meeteor.me/w/68f3a115-4afc-47ff-8d0a-23b6eb4e4d9e")
        }
    })
}
// See the full source in widget/examples/android/