Orishare
Integration prompts

Prompt: Firebase web app

A prompt to paste into a coding agent that integrates a Firebase-authenticated web app with Orishare customer sessions.

Paste the prompt below into your coding agent (Claude Code, Codex, Cursor and similar) from the root of your web app. Replace the values in angle brackets first. The agent needs read access to the docs; it does not need any Orishare secret.

Before you run it, as the operator: create a test publishable key, create an identity connection with profile firebase_id for your Firebase project, mark the events the browser may send as client_allowed, allow your site origin, and enable anonymous exchange if visitors should earn before signing in. The agent guide lists the calls. Give the agent the publishable key and connection id; it never needs a secret key.

For the hosted widget (step 9), also: add your page's exact origin to the portal's allowed_parent_origins and publish the portal (dashboard Widget area, or a proposal you confirm), and add the same origin to the client configuration's allowed_origins. Give the agent the portal id (ptl_...) and the widget origin from the dashboard snippet.

Integrate this web app with Orishare (loyalty and engagement API) using Firebase Authentication.

Read first, and use only what they define:
- https://docs.orishare.com/docs/agent-guide (REST guide; follow its credential matrix at
  https://docs.orishare.com/docs/agent-guide#credential-matrix)
- https://docs.orishare.com/capabilities.json (every supported operation and its credential kinds)
- https://docs.orishare.com/openapi.json (the only source of routes and schemas)
Do not invent routes, fields or rule actions that are not in those files.

Credentials and where they live:
- Publishable key <opk_...> and identity connection id <ic_...>: public build-time config
  (for example VITE_ORISHARE_PUBLISHABLE_KEY, VITE_ORISHARE_CONNECTION_ID). They are not secrets.
- Customer session token: returned by Orishare, kept in memory only. Never write it to
  localStorage, sessionStorage, cookies, URLs or logs.
- Never put a secret key (osk_...) or a personal access token (opat_...) in this codebase or any
  frontend bundle. If you think you need one, stop and tell me which backend call needs it.

Build:
1. A small module orishare.ts with API = 'https://api.orishare.com' (the origin, without /v1;
   every path below already starts with /v1, and receipt_url values are origin-relative).
2. After Firebase sign-in, get the Firebase ID token (auth.currentUser.getIdToken(); not a
   custom token) and exchange it with POST /v1/customer_sessions:
   Authorization: Bearer <publishable key>, Idempotency-Key: one UUID per exchange intent,
   reused on every retry of that exchange, body { grant_type: "identity_token", connection_id,
   provider_token }. Use credentials: 'omit'. If a retried exchange is answered by replay
   (still 201, headers Idempotent-Replayed: true and Idempotent-Replay-Redacted: customer_token,
   body customer_token: null), start a deliberate new exchange: a new intent, a new key and a currently valid Firebase ID token (the same unexpired
   one works). Exchange again the same way when Firebase refreshes the ID token
   (onIdTokenChanged), because a session family cannot outlive its ID token; each exchange
   starts a new session family.
   Anonymous visitors (if enabled): before sign-in, exchange { grant_type: "anonymous" } and keep
   that token in memory. Right after the sign-in exchange, while the anonymous token is still
   live, merge with POST /v1/customer/merges: headers carry the publishable key and the SIGNED-IN
   token, body { source_customer_token: <anonymous token> }, one Idempotency-Key. Exchange alone
   never merges; without this call the anonymous history is lost. Then drop the anonymous token.
3. Send both headers on every customer call: Authorization: Bearer <publishable key> and
   X-Orishare-Customer-Token: <token>.
4. Renew before expires_at with POST /v1/customer/session_renewals (body {}, one
   Idempotency-Key per renewal, reused on its retries); replace the token only when the response has a non-null customer_token.
   On Firebase sign-out call POST /v1/customer/session_family_revocations (it revokes only the
   current family) and clear the token.
5. Send client events with POST /v1/customer/events: a stable event id derived from the
   business action (the same on every send) and one Idempotency-Key per intent, reused on every
   retry. Expect 202 with results[0].status pending or evaluated; poll
   GET /v1/customer/event_receipts/{id} until evaluated, then read all of effects:
   currency_deltas, quest_progress, and when present graph_quest_progress, mechanic_changes,
   coupons, discounts. A resend of the same event id with a new key shows the original event's
   effects again: never render them twice. Only events I list here are
   client-allowed: <lesson.completed, app.opened>. Purchases are sent by our backend, not here.
6. Render state from GET /v1/customer/state and GET /v1/customer/balances (plus
   GET /v1/customer/streaks, GET /v1/customer/badges, GET /v1/customer/quests when the state
   says the mechanic is available). A streak or badge appears only after the customer's first
   contribution to it, and an empty balances list means zero. Match programs by program_key, not
   title. Use If-None-Match with the saved ETag and keep the cached
   body on 304. Treat amounts and state_revision as strings. Render with textContent.
7. Repeat a request only as the guide's "When a request may be repeated" section says
   (https://docs.orishare.com/docs/agent-guide#when-a-request-may-be-repeated): after 429, or 409
   with Retry-After, wait and resend unchanged; after a lost answer, resend only operations whose
   retry class in capabilities.json is atomic_replay or keyed_resume (all customer calls used
   here), with the same Idempotency-Key and body. Never loop on other 4xx; surface error.code.
8. A Firebase anonymous user stays anonymous assurance in Orishare; do not treat it as signed in.
9. If I ask for the hosted widget instead of (or beside) a custom rewards UI, add it as
   https://docs.orishare.com/docs/hosted-widget shows, identity path: the script tag for
   <widget origin>/v1/embed.js (the widget origin is in the dashboard snippet; ask me for it)
   and orishare.push(['mount', { portal: <ptl_...>, publishableKey, connectionId,
   getIdentityToken }]), plus target: '#element' only when the portal's layout is inline; omit
   target for a launcher (GET /v1/public/portals/{portal_id}, no credential, shows
   layout.mode). Use
   getIdentityToken: async () => { await auth.authStateReady();
   return auth.currentUser ? auth.currentUser.getIdToken() : null; }
   It must return null when nobody is signed in and wait for Firebase's initial auth state. The
   loader calls it as soon as the widget's frame has loaded and connected after mount (so the
   exchange happens on page load, even while a launcher is closed) and again whenever the
   widget needs a session (after signIn and after session_needed); it exchanges the token
   itself, so do not refresh or exchange it for the widget. When the user signs out of Firebase
   push ['signOut'] (the loader then revokes the session it minted) and wait for the signed_out
   event before clearing our own state; push ['signIn'] after sign-in; on the
   sign_in_requested event open our sign-in. The page origin must be in the portal's
   allowed_parent_origins and in the client configuration's allowed_origins (operator
   settings; GET /v1/public/portals/{portal_id}, no credential, shows the portal's
   allowed_parent_origins and layout). The widget never sends events and never needs a secret
   key.

Verify against the test environment: sign in, send one allowed event, see its receipt evaluated and the
state refresh. Report the files you changed and anything you could not verify.