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:
Create a widget
In the Meeteor app, go to your event → Embed Widget tab → Configure and save. Copy the widgetId.
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.
<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
| Attribute | Required | Description |
|---|---|---|
data-widget-id | Yes | Your widget ID from the Meeteor dashboard |
data-buyer-name | No | Pre-fill buyer's full name. If valid name + email provided, contact step is skipped. |
data-buyer-email | No | Pre-fill buyer's email address |
data-buyer-phone | No | Pre-fill buyer's phone number |
data-support-registration | No | Set 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.
<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
| Parameter | Description |
|---|---|
theme | dark or light — force color scheme |
name | Buyer's full name (first + last) |
email | Buyer's email address |
phone | Buyer's phone number |
supportRegistration | true — 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.
Install the plugin
Upload and activate the plugin from wordpress-plugin/meeteor-ticket-widget/ (zip the folder first, then Plugins → Add New → Upload Plugin).
Configure (optional)
Go to Settings → Meeteor to set a default Widget ID. This lets you use [meeteor_widget] without specifying an ID.
Add the shortcode
Insert the shortcode in any page or post:
[meeteor_widget widget_id="YOUR_WIDGET_ID"]
Or, if you set a default Widget ID in Settings:
[meeteor_widget]
Shortcode Attributes
| Attribute | Default | Description |
|---|---|---|
widget_id | (from Settings) | Your Meeteor widget ID. Required if no default is set. |
mode | script | script = JS embed (auto-resize), iframe = fixed-height iframe |
height | 600 | Height in pixels (used when mode="iframe") |
support_registration | false | Set 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
Install the package
npm install @meeteor/ticket-widget
Use the component
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
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
Install
npm install @meeteor/ticket-widget-native react-native-webview
Use the component
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
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.
Add the Swift Package in Xcode
Open your Xcode project, then:
- Go to File → Add Package Dependencies
- In the search bar, paste the package URL:
https://gitlab.com/vic2tee4u/meeteor-widget-ios.git
- Set Dependency Rule to "Up to Next Major Version" starting from
0.1.0 - Click Add Package
- 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.
Add to your Info.plist (if needed)
The widget loads web content, so ensure your app allows arbitrary loads or add the widget domain:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
Use in 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
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 }
}
}
}
}
}
}
Use in 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")
)
}
}
Dynamic updates at runtime
After the widget is loaded, you can update buyer info or apply promo codes programmatically:
// 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
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.
Add the dependency
Add the module to your build.gradle.kts:
implementation("me.meeteor:widget:1.0.0")
Jetpack 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()
)
}
XML / View System
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
| Method | When to Use |
|---|---|
URL params (?name=...&email=...) | iframe and direct links |
Data attributes (data-buyer-name) | JavaScript embed |
buyer prop | React, React Native, Swift, Kotlin |
meeteor:set_contact postMessage | Dynamic updates after load |
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:
- Data attribute:
data-support-registration="true"on the container (JavaScript embed) - URL param:
?supportRegistration=true(iframe or direct link) - Widget config:
support_registration: truefrom the backend
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:
| Method | Example |
|---|---|
| URL parameter | ?theme=dark or ?theme=light |
| System preference | Auto-detected via prefers-color-scheme |
| Theme colors | If background_color luminance < 0.2, dark mode activates automatically |
| postMessage | Send 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.
| Layout | Max Width | Image | Best For |
|---|---|---|---|
vertical | 480px | Top, 16:9 | Standard embed (default) |
horizontal | 720px | Left, 40% width | Wide containers, sidebars with space |
compact | 380px | Hidden | Tight sidebars, popups |
full | 560px | Top, 2:1 | Dedicated ticket pages |
Theme Options
Configure these in the Meeteor app or override via meeteor:set_theme postMessage:
| Property | Type | Default | Description |
|---|---|---|---|
primary_color | hex | #6C63FF | Primary brand color (buttons, accents) |
secondary_color | hex | #FFFFFF | Secondary color |
background_color | hex | #FFFFFF | Widget background. Dark values auto-enable dark mode. |
text_color | hex | #1A1A2E | Primary text color |
font_family | string | Inter | CSS font-family name. Any Google Font is loaded automatically. |
font_url | string | null | null | URL to a custom font CSS stylesheet (for non-Google fonts). When set, this is loaded instead of Google Fonts. |
border_radius | number | 12 | Corner radius in px |
button_style | string | filled | filled, outlined, or gradient |
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 Type | Payload | Description |
|---|---|---|
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 |
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)
| Event | Payload | Description |
|---|---|---|
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:close | — | User requested to close the widget. |
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
| Event | Trigger |
|---|---|
purchase.completed | A ticket purchase was successfully completed via the widget. |
purchase.failed | A payment attempt failed (card declined, etc.). |
widget.view | A user viewed / loaded the widget. |
sponsor.view_completed | A user watched a full sponsor video. |
discount.applied | A 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
| Header | Description |
|---|---|
X-Meeteor-Signature | HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret. |
X-Meeteor-Event | The event type (e.g. purchase.completed). |
X-Meeteor-Delivery | Unique delivery ID for idempotency. |
X-Meeteor-Timestamp | Unix 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.
{
"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
}
}
}
subtotal: 3500 = $35.00. Always divide by 100 for display.
purchase.failed
{
"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
{
"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
{
"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
{
"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"
}
}
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.
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/