Skip to content

Channel Messaging

Feature Type

PWA Editor Feature - Real-time messaging from the PWA editor to your application.

The PWA editor sends real-time messages to the parent application at key points during the user journey - the project was saved, the user is leaving, checkout has started, an order completed.

Messages are delivered over postMessage or the platform's native bridge:

PlatformTransport
Web (iframe)postMessage events - see the demo
React Nativewindow.ReactNativeWebView.postMessage(...)
iOS (WKWebView)window.webkit.messageHandlers.appHandler.postMessage(...)
Android (WebView)window.AndroidPbaiAtelier.sendToNative(...)

Android Bridge Name

The Android interface is registered as AndroidPbaiAtelier. Registering a different name silently drops every message.

Embedding In A Native App

Receiving these messages is only part of hosting the editor in a native shell. Back navigation, JavaScript dialogs and the file picker each need host-side wiring - see WebView Integration.

Overview

Channel messages tell you about:

  • Save events during editing
  • The user arriving in, and leaving, the editor
  • Navigation between editor and checkout
  • Progress through checkout steps
  • Order completion

Interactive Demo

Try the Channel Messaging Demo for a generic sample of how channel messages work.

Which Checkout Flow Applies To You

This is the biggest branch in the integration, and it is set by your client configuration rather than chosen at runtime. Establish which one you are on before reading further - it decides whether half of this document applies to you.

Flow A: Internal CheckoutFlow B: External Checkout
Who handles checkoutThe editor's built-in UI - cart, address, payment, confirmationYour application, with your own UI
On orderclose: false - keep the editor openclose: true - tear the editor down
Checkout step eventsYou receive them allNone are ever sent
Order signalconfirmation channel messageYour own systems, or Server-Side Order Tracking

Both flows send the same editor events. They diverge from the moment the user clicks checkout - see User Initiates Checkout.

Message Format

Every message except openExternalUrl carries the same base fields:

json
{
  "close": false,
  "checkout": false,
  "action": "save",
  "ui": "mobile",
  "route": {
    "view": "editor",
    "step": 1
  }
}

Base Message Fields

FieldTypeDescription
closebooleanWhether the editor is finished and the view should be closed - see below
checkoutbooleanWhether the user has entered the checkout process
actionstringWhat happened - see the Action Reference
uistringUI mode: "mobile" or "desktop"
route.viewstringCurrent page name (e.g. "editor", "checkout", "upload")
route.stepintegerStep number for multi-step processes

The close Field

close is the field with consequences: it is the editor telling you it has finished with the user and your shell should take over. Everything else is informational.

Act on close, not on action. The set of actions will grow over time, and a handler written as if (close) closeWebview() keeps working when it does. A handler that lists exit actions by name will silently miss new ones.

The one thing close does not do is save anything for you - it fires after the editor has persisted its own state, so the project is already safe by the time you receive it.

Action Reference

Every action the editor can send. close and checkout are the values that accompany it.

ActionFires whenclosecheckout
editorOpenedEvery editor page loadfalsefalse
projectStartedUser arrives in the editor from photo selectionfalsefalse
savePeriodically, as the user editsfalsefalse
saveAndExitUser saves and confirms exittruefalse
clearPagebuildUser discards progress and exitstruefalse
deleteProjectUser deletes the project and exitstruefalse
order (Flow A)User starts checkout, editor handles itfalsetrue
order (Flow B)User starts checkout, you handle ittruetrue
summaryCheckout step 1 → 2falsetrue
addressCheckout step 2 → 3falsetrue
paymentCheckout step 3 → 4falsetrue
confirmationUser lands on the confirmation pagetruetrue
summaryBackUser leaves summary, back to the editorfalsetrue
addressBackUser leaves address, back to summaryfalsetrue
paymentBackUser leaves payment, back to addressfalsetrue
openExternalUrlA URL must be opened outside the webviewabsentabsent

order Means Two Different Things

order is sent in both checkout flows, and the flows want opposite behaviour from you. Flow A sends close: false and expects the editor to stay open so it can run checkout itself; Flow B sends close: true and expects you to tear it down and take the user to your own checkout.

Branch on close, never on action === 'order' alone.

Additional Data

Depending on the current route, messages include either project data or checkout data.

Project Data

Included during editor events:

json
{
  "title": "Family Vacation 2024",
  "preview": "https://pwa-pbai-dev-attachments.s3-accelerate.amazonaws.com/u-abc/jonh123456/projects/p-def/preview.jpeg",
  "link": "https://{frontend}.photobook.ai/editor?id=p-abc123",
  "id": "p-abc123",
  "endUserId": "user-provided-id",
  "product": { /* product details */ },
  "metadata": {
    "currentPages": 24,
    "photoCount": 40,
    "usedPhotoCount": 18,
    "productName": "Premium Photo Book"
  }
}
FieldTypeDescription
titlestringUser-entered title (or product SKU if not set)
previewstringURL to project preview image
linkstringDirect link to editor for this project
idstringUnique project ID
endUserIdstringUser ID provided by parent client
metadata.currentPagesintegerNumber of pages in project
metadata.photoCountintegerPhotos in the project's pool, whether or not they have finished uploading
metadata.usedPhotoCountintegerDistinct photos placed in the book - a photo used on several pages counts once
metadata.productNamestringName of the product

The Two Photo Counts Answer Different Questions

photoCount is the size of the user's photo pool - everything they have committed to the project, counted the moment they commit it and regardless of whether the upload has finished. It does not climb during an upload.

usedPhotoCount is how many distinct photos have actually been placed in the book. It is always less than or equal to photoCount, and it counts a photo once no matter how many pages it appears on.

Checkout Data

Included during checkout events, if your client configuration uses the editor's internal checkout. Contents vary by step:

Summary Step:

json
{
  "results": [
    {
      "quantity": 2,
      "extraCostPerPage": 0.50,
      "price": 29.99,
      "total": 65.98
    }
  ]
}

Address Step:

json
{
  "results": {
    "firstName": "John",
    "lastName": "Doe",
    "address1": "123 Main St",
    "address2": "Apt 4B",
    "city": "New York",
    "state": "NY",
    "postcode": "10001",
    "country": "United States",
    "isoCode2": "US",
    "isoCode3": "USA",
    "telephone": "+1234567890"
  }
}

Payment Step:

json
{
  "orderId": 12345,
  "voucher": "SAVE10",
  "paymentMethod": "stripe",
  "cartSubTotal": 65.98,
  "shippingCostTotal": 5.99,
  "taxTotal": 3.60,
  "couponTotal": 6.60,
  "grandTotal": 68.97,
  "items": [ /* line items */ ],
  "shippingAddress": { /* address object */ }
}

Message Scenarios

The first scenario below shows a complete payload. The rest show only the fields that distinguish them - every editor event otherwise carries the Project Data shape above.

Editor Events

Auto-Save During Editing

Sent periodically as the user edits. This is the full shape every other editor event follows:

json
{
  "title": "Summer Memories",
  "preview": "https://pwa-pbai-dev-attachments.s3-accelerate.amazonaws.com/u-abc/jonh123456/projects/p-def/preview.jpeg",
  "link": "https://{frontend}.photobook.ai/editor?id=p-def",
  "id": "p-def",
  "createdAt": 1774959510786,
  "updatedAt": 1774959594788,
  "expiresAt": 1790770710,
  "product": {
    /* Product configuration including dimensions, specifications, pricing, and PSP details */
  },
  "endUserId": "jonh123456",
  "clientName": "yourclient",
  "metadata": {
    "costPerPage": 0.5,
    "currentPages": 42,
    "basePages": 24,
    "basePrice": 12,
    "photoCount": 43,
    "usedPhotoCount": 43,
    "productName": "Hard-21-S-190gsm"
  },
  "close": false,
  "checkout": false,
  "action": "save",
  "ui": "desktop",
  "route": {
    "view": "editor",
    "step": 1
  }
}

What to do:

  • Update project status in your database
  • Show save indicator to user
  • Track progress

Editor Opened

Sent on every editor page load, once project data has resolved. This is the dependable "the user is now in the editor" signal - it fires on a fresh visit, a reload, a return from checkout, and the hand-off from photo selection alike.

json
{
  "close": false,
  "checkout": false,
  "action": "editorOpened",
  "route": { "view": "editor", "step": 1 }
  // ... plus the full projectData shape
}

What to do:

  • Adjust your app's chrome for the editor view
  • Record that the user resumed an existing project
  • Use it as the anchor for session or engagement timing

Project Started

Sent when the user arrives in the editor directly from photo selection - a brand-new book beginning its build. It is followed immediately by editorOpened, so on this path your handler sees both, in that order.

The upload is still running at this moment, but photoCount already reports the full pool the user committed:

json
{
  "close": false,
  "checkout": false,
  "action": "projectStarted",
  "metadata": {
    "photoCount": 40,
    "currentPages": 24,
    "productName": "Hard-21-S-190gsm"
  }
  // ... plus the rest of the projectData shape
}

What to do:

  • Mark the project as active in your database - this is the point a selection becomes a real book
  • Show progress affordances, since the upload and page build run for a while after this
  • Record how many photos the user committed to

This Is Not An Upload-Complete Signal

projectStarted marks the beginning of the upload, not its end - photos continue uploading and being processed for some time afterwards. photoCount tells you how many the user committed, not how many have landed, and usedPhotoCount is absent until the book has been laid out.

User Saves and Exits Editor

Sent when the user saves and confirms exit:

json
{
  "close": true,
  "checkout": false,
  "action": "saveAndExit",
  "route": { "view": "editor", "step": 1 }
  // ... plus the full projectData shape
}

What to do:

  • Close the webview/iframe
  • Save project state
  • Bring the user back to the page you want them to land

User Discards Progress and Exits Editor (leaving any uploaded photos intact)

Sent when the user discards progress - the book layout, not the photos - and confirms exit:

json
{
  "close": true,
  "checkout": false,
  "action": "clearPagebuild",
  "route": { "view": "editor", "step": 1 }
  // ... plus the full projectData shape
}

What to do:

  • Close the webview/iframe
  • Save project state (project is reset to an empty state)
  • Bring the user back to the page you want them to land

User Deletes Project and Exits Editor

Sent when the user deletes the project and confirms exit, usually when a new empty project was generated and the user leaves before uploading anything. Note the route.view is upload, and no project data is included - there is no project left to describe:

json
{
  "close": true,
  "checkout": false,
  "action": "deleteProject",
  "ui": "desktop",
  "route": {
    "view": "upload",
    "step": 1
  }
}

What to do:

  • Close the webview/iframe
  • The project is deleted, so there's no need to save it
  • Bring the user back to the page you want them to land

User Initiates Checkout

This is where the two flows diverge. Both send action: "order" and checkout: true; they differ in close.

Flow A: Internal Checkout (Editor handles checkout)
json
{
  "close": false,
  "checkout": true,
  "action": "order",
  "route": { "view": "editor", "step": 1 }
  // ... plus the full projectData shape
}

What to do:

  • Track conversion funnel
  • Keep the webview/iframe open - the checkout flow begins inside the editor
  • The editor will guide the user through the checkout steps below
Flow B: External Checkout (Your application handles checkout)
json
{
  "close": true,
  "checkout": true,
  "action": "order",
  "route": { "view": "editor", "step": 1 }
  // ... plus the full projectData shape
}

What to do:

  • Close the webview/iframe
  • Use the project and product data to add the item to your cart
  • Navigate to your application's checkout flow
  • Process the order using your own payment and fulfillment system

Checkout Events (Flow A Only)

Applies to Internal Checkout Only

These events are only sent under Flow A. Under Flow B they never occur - checkout happens entirely on your systems.

Each step forward carries that step's Checkout Data.

Transitionactionroute.stepPayload
Summary → Addresssummary1results: cart items
Address → Paymentaddress2results: shipping address
Payment → Confirmationpayment3Full order details

All carry close: false, checkout: true and route.view: "checkout".

json
{
  "close": false,
  "checkout": true,
  "action": "payment",
  "ui": "desktop",
  "route": {
    "view": "checkout",
    "step": 3
  },
  "orderId": 12345,
  "grandTotal": 68.97,
  "items": [ /* order items */ ],
  "shippingAddress": { /* address */ }
}

What to do:

  • Save order details
  • Track successful conversions
  • Prepare confirmation UI

Order Confirmation Page

Sent when the user lands on the confirmation page. This is the one checkout event carrying close: true:

json
{
  "close": true,
  "checkout": true,
  "action": "confirmation",
  "ui": "desktop",
  "route": {
    "view": "checkout",
    "step": 4
  }
}

What to do:

  • Close webview/iframe
  • Show order confirmation in your app
  • Display order tracking information

Going Back A Step

The user can retreat through the checkout steps. These mirror the forward transitions and carry no payload beyond the base fields.

The user leavesReturning toactionroute.step
SummaryEditorsummaryBack1
AddressSummaryaddressBack2
PaymentAddresspaymentBack3
json
{
  "close": false,
  "checkout": true,
  "action": "summaryBack",
  "ui": "desktop",
  "route": {
    "view": "checkout",
    "step": 1
  }
}

What to do:

  • Adjust any funnel tracking - the user has stepped backwards, not abandoned
  • No action needed on the view itself; the editor handles the navigation

Opening External URLs

Different Message Shape

Unlike every message above, this one does not include the Base Message Fields (close, checkout, route). It is a minimal, standalone payload.

Some interactions started inside the editor need to open a URL outside the embedded webview/iframe - the Google Photos picker, OAuth consent screens. Native wrappers silently swallow window.open, so the editor asks the host shell to open the URL instead.

json
{
  "action": "openExternalUrl",
  "url": "https://photos.google.com/picker/session-id/autoclose"
}
FieldTypeDescription
actionstringAlways "openExternalUrl" for this message
urlstringThe external URL to open

What to do:

  • React Native: open url via Linking.openURL(url) or an in-app browser (e.g. expo-web-browser)
  • iOS / Android: open url in the system browser or an in-app browser tab
  • Web (iframe): no action needed - see below

Web Fallback

On web, if no native bridge (ReactNativeWebView, webkit.messageHandlers.appHandler, or AndroidPbaiAtelier) is detected, the editor falls back to window.open(url, '_blank') directly, so this message is only sent from native contexts.

Implementation

The handler shape is the same everywhere; only the transport differs. Branch on close first, then on action for the events you care about.

javascript
// Web (iframe)
window.addEventListener('message', (event) => {
  // Verify origin for security
  if (event.origin !== 'https://{frontend}.photobook.ai') return

  handleChannelMessage(event.data)
})

function handleChannelMessage(message) {
  const { close, checkout, action } = message

  // openExternalUrl carries none of the base fields, so it is handled first
  if (action === 'openExternalUrl') {
    openInSystemBrowser(message.url)
    return
  }

  // the editor is finished - this covers saveAndExit, clearPagebuild,
  // deleteProject, confirmation, and Flow B's order, without naming any of them
  if (close) {
    closeWebview()
    return
  }

  if (action === 'projectStarted') markProjectActive(message.id, message.metadata)
  if (action === 'save') saveProjectProgress(message.id, message.metadata)
  if (checkout && action === 'order') analytics.track('checkout_started')
}

React Native receives the same payload as a JSON string:

javascript
import { WebView } from 'react-native-webview'

function EditorWebView({ url }) {
  const handleMessage = (event) => {
    handleChannelMessage(JSON.parse(event.nativeEvent.data))
  }

  return <WebView source={{ uri: url }} onMessage={handleMessage} javaScriptEnabled={true} />
}

iOS delivers messages to WKScriptMessageHandler on the appHandler name; Android to the AndroidPbaiAtelier interface as a JSON string. Both then follow the same branching as above - see WebView Integration for the surrounding host-side setup.

photobook.ai Developer Documentation