Issue in Facebook browser
August 25, 2025 · AI Conversations
So you mean **on an external browser (Chrome, Safari, etc.) everything works fine**, but the issue only happens **inside the Facebook in-app browser**, right?
That usually happens because Facebook’s in-app browser has stricter rules or missing features — especially around **iframes, CSP (Content Security Policy), cookies, and Stripe/Google Pay scripts**.
👉 Can you confirm what exactly is failing inside the FB in-app browser? For example:
– Is it the **Stripe card fields not showing / not clickable**?
– Or the **Google Pay button not rendering**?
– Or something else like redirect / post-payment response?
If you tell me which part breaks, I can suggest the exact fix.
# 1) Fix CSP (most common cause)
A “ won’t apply `frame-ancestors` (Facebook even logs this). Set CSP via **HTTP headers** and include Stripe + Google Pay domains.
### Apache (htaccess or vhost)
<IfModule mod_headers.c>
# DO NOT use meta CSP for frame-ancestors — it’s ignored.
Header set Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline' https://js.stripe.com https://pay.google.com https://www.gstatic.com https://*.gstatic.com;
frame-src 'self' https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://payments.google.com;
connect-src 'self' https://api.stripe.com https://r.stripe.com https://m.stripe.network https://m.stripe.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://q.stripe.com https://*.stripe.com;
base-uri 'self';
frame-ancestors 'self';
"
# Optional but good hygiene:
Header always set X-Content-Type-Options "nosniff"
</IfModule>
### Nginx
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline' https://js.stripe.com https://pay.google.com https://www.gstatic.com https://*.gstatic.com;
frame-src 'self' https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://payments.google.com;
connect-src 'self' https://api.stripe.com https://r.stripe.com https://m.stripe.network https://m.stripe.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://q.stripe.com https://*.stripe.com;
base-uri 'self';
frame-ancestors 'self';
" always;
Notes:
– You already added `frame-ancestors` for Google Pay in meta; move **all CSP** to headers.
– Add **`https://js.stripe.com`** (scripts & frames) and **`https://m.stripe.network`** (telemetry).
– Keep `frame-ancestors` (self) and avoid `*` here.
# 2) Remove `apiVersion` from `Stripe(…)` on the client
You posted:
VersionError: Unsupported on version [basil]: Can not provide apiVersion to Stripe()
That means you’re doing something like:
const stripe = Stripe('pk_live_xxx', { apiVersion: '2020-08-27' }); // ❌ don’t
Fix:
const stripe = Stripe('pk_live_xxx'); // âś… no apiVersion here
(Use API version **only server-side** in Stripe PHP client; not in Stripe.js init.)
# 3) Check for CSS/overlay issues that block taps
Inside FB in‑app, tiny layout quirks can block iframe inputs:
– **No overlay** above the Elements iframe. Inspect for any element covering it (`position: absolute/fixed`, `z-index`, `pointer-events: none/auto`).
– Avoid `transform`, `filter`, or `opacity` on ancestor containers of Elements — iOS WebViews sometimes break focus on iframes inside transformed parents.
– Ensure the iframe has a visible height (no `overflow: hidden` collapsing a parent).
Quick test: temporarily add this in dev tools to the wrapper around Elements:
.stripe-elements-wrapper, .ElementsApp, .InputElement, iframe {
position: relative !important;
z-index: 2 !important;
}
# 4) Don’t trap clicks/touches
Make sure you’re not cancelling touch/clicks:
element.addEventListener('touchstart', e => {
// e.preventDefault(); // ❌ this can block focus on iframes/inputs
});
# 5) Feature‑detect the FB in‑app browser and offer “Open in Browser”
As a fallback (and for better conversion), detect FB/IG in‑app and show a notice + button:
const ua = navigator.userAgent || '';
const isFacebookIAB = /bFBAN|FBAV|FB_IAB|Instagramb/i.test(ua);
if (isFacebookIAB) {
// Show a banner to open in the device browser
// e.g., window.open(location.href, '_blank', 'noopener'); on a user tap
}
# 6) For Google Pay (if used)
If you also render Google Pay:
– Keep `https://pay.google.com`, `https://payments.google.com`, and `https://*.gstatic.com` in `script-src` & `frame-src`.
– Don’t restrict `allow` attributes on any payment iframe.
—
## Fast checklist for you
– [ ] Move CSP to **server headers** (no meta). Include Stripe + Google Pay domains above.
– [ ] Remove `apiVersion` from client `Stripe(…)`.
– [ ] Inspect CSS: no overlays, no transforms on parents, proper z-index.
– [ ] Don’t call `preventDefault` on touch for Elements container.
– [ ] (Optional) Detect FB IAB and show “Open in external browser”.
If you share your current **CSP header** and the snippet where you **initialize Stripe** + the **Elements mount CSS**, I’ll plug in the exact values for your setup.
Header unset ETag
Header append Vary: Accept-Encoding
we have aleardy this
Replace your header with a **single consolidated CSP** like this (Apache):
<IfModule mod_headers.c>
# ONE consolidated CSP (avoid sending multiple CSP headers)
Header always set Content-Security-Policy "
default-src 'self';
base-uri 'self';
form-action 'self' https://hooks.stripe.com;
script-src 'self' 'unsafe-inline'
https://js.stripe.com
https://pay.google.com https://www.gstatic.com https://*.gstatic.com;
frame-src 'self'
https://js.stripe.com https://hooks.stripe.com
https://pay.google.com https://payments.google.com;
connect-src 'self'
https://api.stripe.com https://r.stripe.com
https://m.stripe.network https://m.stripe.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://q.stripe.com https://*.stripe.com;
worker-src 'self' blob:;
frame-ancestors 'self';
"
Header unset ETag
Header append Vary "Accept-Encoding"
</IfModule>
Why this fixes FB in‑app:
– **script-src** allows Stripe/Google Pay JS to load.
– **frame-src** allows their iframes to render and receive focus.
– **connect-src** allows XHR/WebSocket/telemetry to Stripe.
– **form-action** allows Stripe’s SCA/redirect posts.
– **worker-src** covers Stripe’s web workers when used.
– Keeping **frame-ancestors 'self'** is fine (delivered via header, not meta).
Also double‑check these two quick things:
1) **Client init** — no `apiVersion` in `Stripe()`:
const stripe = Stripe('pk_live_or_test'); // âś… no apiVersion here
2) **CSS/overlay** — nothing covering the iframe:
– No `position:fixed/absolute` overlays on top.
– Avoid `transform` on ancestor containers of the Elements mount.
– Don’t `preventDefault()` on touch/click for the Elements wrapper.
### How to verify
– Run: `curl -I https://your-domain/…` and confirm the **single** `Content-Security-Policy` header matches the one above.
– In FB in‑app browser, open the page → Menu (…) → open **Browser console** (device logs) and check there are **no CSP violations**.
– If you still can’t focus the fields, I’ll review your Elements mount HTML/CSS (paste that snippet) and we’ll spot any overlay/z-index issues fast.
Try this safe, one‑line version first:
<IfModule mod_headers.c>
Header set Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self' https://hooks.stripe.com; script-src 'self' 'unsafe-inline' https://js.stripe.com https://pay.google.com https://www.gstatic.com https://*.gstatic.com; frame-src 'self' https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://payments.google.com; connect-src 'self' https://api.stripe.com https://r.stripe.com https://m.stripe.network https://m.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://q.stripe.com https://*.stripe.com; worker-src 'self' blob:; frame-ancestors 'self'"
Header unset ETag
Header append Vary "Accept-Encoding"
</IfModule>
Notes:
– **Single line only** (no line breaks inside the quoted value).
– Keep single quotes around `'self'` exactly as shown.
– Works on older Apache because it uses `Header set` (not `Header always set`).
If you’re on a modern Apache and prefer `always`, use:
<IfModule mod_headers.c>
Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; form-action 'self' https://hooks.stripe.com; script-src 'self' 'unsafe-inline' https://js.stripe.com https://pay.google.com https://www.gstatic.com https://*.gstatic.com; frame-src 'self' https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://payments.google.com; connect-src 'self' https://api.stripe.com https://r.stripe.com https://m.stripe.network https://m.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://q.stripe.com https://*.stripe.com; worker-src 'self' blob:; frame-ancestors 'self'"
</IfModule>
If it **still** 500s:
1) Your server may not allow headers in `.htaccess` (AllowOverride off). Move the same line into the **vhost** config instead, or ask host to enable it.
2) `mod_headers` might not be loaded. Temporarily remove the “ wrapper and see the exact error in the log; or ensure it’s enabled (`a2enmod headers` on Ubuntu/Debian).
3) Check for stray smart quotes or extra spaces introduced by copy/paste.
### Verify quickly
– Run: `curl -I https://your-domain/path` and make sure you see exactly one `Content-Security-Policy:` header.
– Remove any “ tags from HTML (or they’ll conflict/ confuse).
Once this header is live, re-test Stripe Elements in the FB in‑app browser. If placeholders still don’t show or inputs aren’t focusable, send me your Elements mount HTML/CSS and we’ll check for overlay/z-index/transform issues next.
# 1) Prove FB WebView is actually getting your CSP header
Sometimes proxies/CDNs serve different headers to in‑app browsers.
Add this tiny endpoint (PHP) at `/debug/headers` and open it **inside** the Facebook in‑app browser:
<?php
header('Content-Type: text/plain');
foreach (getallheaders() as $k=>$v) echo "$k: $vn";
You should see **Content-Security-Policy: …** exactly as you set it.
If it’s missing or different there (but present with `curl -I`), fix your CDN/origin rules so the **header** (not meta) is served consistently.
Also add this in your page to catch CSP errors (temporary):
<script>
window.addEventListener('securitypolicyviolation', e => {
console.log('CSP VIOLATION:', e.blockedURI, e.effectiveDirective);
});
</script>
Open the page in FB IAB → check device logs (you’ll see which directive still blocks Stripe).
# 2) Neutralize the usual CSS blockers in webviews
Paste this CSS (temporary) and re‑test in FB IAB:
/* Parents of the Elements mount */
#card-element, .stripe-elements-wrapper, .ElementsApp {
position: relative !important;
z-index: 2 !important;
min-height: 48px !important;
}
/* Never let a parent kill pointer events */
#card-element * { pointer-events: auto !important; }
/* Enable text selection/focus on iOS WebViews */
body, input, iframe, .ElementsApp {
-webkit-user-select: text !important;
user-select: text !important;
}
/* Remove accidental overlay behavior */
.overlay, .modal, .drawer {
pointer-events: auto !important;
}
/* If any ancestor had these, they can break focusing iframes on iOS */
.has-transform, [style*="transform"], [class*="transform"] {
/* Prefer removing transforms near the card element entirely */
}
/* If your container is sticky/fixed, try making it static while testing */
.payment-sticky, .header-sticky {
position: static !important;
}
And make sure the Elements mount is **not** inside a container with `transform`, `filter`, `opacity
// ❌ Blocks iframe focus in webviews
el.addEventListener('touchstart', e => { e.preventDefault(); });
// or globally:
document.addEventListener('touchmove', e => e.preventDefault(), { passive: false });
// or CSS:
.payment-container { touch-action: none; } /* ❌ */
If you need touch handling, do it on non‑input regions and never on the Elements wrapper.
# 4) Force focus on a user tap (workaround)
Sometimes the frame renders but FB IAB doesn’t transfer focus. Add a “focus nudge”:
const stripe = Stripe('pk_test_or_live');
const elements = stripe.elements();
const card = elements.create('card', { hidePostalCode: true });
card.mount('#card-element');
document.getElementById('card-element').addEventListener('click', () => {
try { card.focus(); } catch (e) {}
});
// Also, a small delay after mount can help FB IAB
setTimeout(() => { try { card.focus(); } catch(e){} }, 600);
# 5) FB/IG in‑app specific guardrail (best practice)
Show an “Open in browser” prompt **only when** it’s the FB/IG WebView:
const ua = navigator.userAgent || '';
const isIAB = /bFBAN|FBAV|FB_IAB|Instagramb/i.test(ua);
if (isIAB) {
// UX banner: “For secure payments, open in your browser”
// On user tap:
window.location.href = 'https://your-domain.tld/pay?open=1';
// or instruct them to use the menu ••• > “Open in Browser”
}
—
## Quick sanity list (re‑check)
– You **do** have a single **header** CSP (not meta), including:
– `script-src https://js.stripe.com https://pay.google.com https://www.gstatic.com https://*.gstatic.com`
– `frame-src https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://payments.google.com`
– `connect-src https://api.stripe.com https://r.stripe.com https://m.stripe.network https://m.stripe.com`
– `frame-ancestors 'self'` (fine)
– Client init is `Stripe('pk_…')` **without** `apiVersion`.
– No overlays/`transform`/`sticky`/`touch-action:none` over the mount.
– No `preventDefault()` on touch in the payment area.
– Card iframe actually loads (Network tab shows `js.stripe.com` & `api.stripe.com` OK).
If you want, paste:
1) The **response headers** you see from `/debug/headers` in the FB browser,
2) Your **Elements mount HTML/CSS**, and
3) The snippet where you add any touch/mousedown listeners.
I’ll pinpoint the blocker and give you a minimal patch.
.section h3 { color: #ffcb0e;font-size: 1.5em;}
#event-order { position: relative; width: 100%; height: auto; }
#event-order.confirmation { font-size: 1.4em; line-height: 1.5em; }
#event-order button, #event-order input, #event-order select, #event-order textarea { display: block; padding: 0em 1em; border-radius: 5px; margin: 0.3em auto; width: 100%; height: 3em; line-height: 3em; color: rgb(25, 25, 25); background-color: rgb(255,255,255); font-weight: bold; font-size: 1em; border: 0; /*-webkit-appearance: none;*/ }
#event-order .event-header { width: 100%; height: auto; background-color: rgb(237, 30, 38); color: rgb(255, 255, 255); padding: 1.2em 1em 1em 1em; text-align: center; display: inline-block; }
.event-header h1 { /*float: left;*/ line-height: 1em; }
.event-header h2 { /*float: left;*/ line-height: 1.5em; margin-left: 20px; }
#event-order .event-content { width: 100%; height: auto; color: rgb(255, 255, 255); padding: 1em 1em 2em 1em; background: radial-gradient( #006fb4, #1d3f78); }
#event-order .section { padding: 0.5em 1em; clear: both; text-align: center; }
#event-order .total { font-size: 2em;}
#event-order .section-contact { background-image: url(/wp-content/themes/fossetts-circus/hatching.png); background-size: cover; height: auto; display: inline-block; width: 100%; border: 10px solid transparent; -webkit-border-image: url(/wp-content/themes/fossetts-circus/stroke-border.png) 23 round; /* Safari 3.1-5 */ -o-border-image: url(/wp-content/themes/fossetts-circus/stroke-border.png) 23 round; /* Opera 11-12.1 */ border-image: url(/wp-content/themes/fossetts-circus/stroke-border.png) 23 round; }
#event-order .section-event { background-image: url(/wp-content/themes/fossetts-circus/hatching.png); background-size: cover; height: auto; display: inline-block; width: 100%; border: 10px solid transparent; -webkit-border-image: url(/wp-content/themes/fossetts-circus/stroke-border.png) 23 round; /* Safari 3.1-5 */ -o-border-image: url(/wp-content/themes/fossetts-circus/stroke-border.png) 23 round; /* Opera 11-12.1 */ border-image: url(/wp-content/themes/fossetts-circus/stroke-border.png) 23 round; }
#event-order .section-payment { background-image: url(/wp-content/themes/fossetts-circus/hatching.png); background-size: cover; height: auto; display: inline-block; width: 100%; border: 10px solid transparent; -webkit-border-image: url(/wp-content/themes/fossetts-circus/stroke-border.png) 23 round; /* Safari 3.1-5 */ -o-border-image: url(/wp-content/themes/fossetts-circus/stroke-border.png) 23 round; /* Opera 11-12.1 */ border-image: url(/wp-content/themes/fossetts-circus/stroke-border.png) 23 round; }
#event-order .section-payment #payment-fields { display: block; padding: 1em; border-radius: 5px; margin: 1em auto; width: 100%; height: 3em; color: rgb(66, 109, 143); background-color: rgb(255,255,255); font-weight: bold; }
#event-order .section-payment .voucher { margin: 0.72em auto; }
#event-order .section-payment .voucher .warning-voucher { background-color: rgb(237, 30, 38); color: white; margin-top: 1em; }
#event-order .section-payment .voucher .success-voucher { background-color: rgb(30, 187, 38); color: white; margin-top: 1em; }
#event-order .section-info { }
#event-order .section-separator { width: 70%; height: 1px; margin: 1px auto; /*background-color: rgb(255, 255, 255);*/ }
#event-order hr { border: none;}
#event-order .order-submit { background-color: rgb(237, 30, 38); color: rgb(255, 255, 255); border-radius: 0px; font-size: 2em; padding: 0 0 0 0; margin: 0; }
#event-order .order-submit:disabled { background-color: rgba(237, 30, 38,0.5); }
#event-order .warning { display: none; clear: both; }
#event-order .form-errors, #event-order .warning { color: rgb(255, 255, 255); background-color: rgb(237, 30, 38); font-weight: bold; padding: 0.5em; }
@media (max-width: 480px) {
.poster-inner {
padding: 20px;
}
.event-title {
font-size: 1.8rem;
}
.date-day {
font-size: 1.8rem;
}
.event-date-col {
min-width: 120px;
padding: 10px;
}
.section-container.slider {
height: 250px;
}
.master-shield {
max-width: 330px;
}
}
@media only screen and (min-width: 768px) {
#event-order .input-wrapper-1 { display: block; width: 100%; padding: 0em 1em; float: left;
}
#event-order .input-wrapper-2 { display: block; width: 50%; padding: 0em 1em; float: left; }
#event-order .input-wrapper-3 { display: block; width: 33.3333%; padding: 0em 1em; float: left; }
}
@media only screen and (max-width: 768px) {
#event-order .event-header { text-align: center; }
}
/* Header and Navigation Styles */
.site-header {
position: absolute;
top: -10px;
left: 0;
right: 0;
z-index: 999;
/* background: rgba(255, 255, 255, 0.95); */
/* backdrop-filter: blur(10px); */
/* border-bottom: 2px solid #ed1e26; */
padding: 10px 0;
}
.header-inner {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
}
.logo-link {
display: block;
text-decoration: none;
}
.site-logo {
height: 150px;
width: auto;
max-width: 260px;
}
.flyout-logo {
height: 50px;
width: auto;
max-width: 150px;
}
/* Hamburger Menu */
.hamburger {
display: flex;
flex-direction: column;
justify-content: center;
width: 70px;
height: 60px;
background: transparent;
border: 2px solid #000000;
cursor: pointer;
padding: 0;
background: #f7cd39;
padding: 20px 10px;
gap: 10px;
border-radius: 9px;
align-content: center;
align-items: center;
}
.hamburger span {
width: 100%;
height: 5px;
border: #252525 3px solid;
border-radius: 7px;
transition: all 0.3s ease;
z-index: 999;
gap: 20px;
}
.hamburger:hover span {
background: #1d3f78;
}
/* Flyout Menu */
.flyout-menu {
position: fixed;
top: 0;
right: -100%;
width: 300px;
height: 100vh;
background: white;
box-shadow: -5px 0 15px rgba(0, 0, 0, 0.1);
transition: right 0.3s ease;
z-index: 1002;
overflow-y: auto;
}
.flyout-menu.open {
right: 0;
}
.flyout-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #eee;
background: #f8f9fa;
}
.close-flyout {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #ed1e26;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-flyout:hover {
color: #1d3f78;
}
.flyout-menu ul {
list-style: none;
padding: 0;
margin: 0;
}
.flyout-menu li {
border-bottom: 1px solid #eee;
}
.flyout-menu a {
display: block;
padding: 15px 20px;
color: #333;
text-decoration: none;
font-weight: 600;
font-size: 16px;
transition: all 0.3s ease;
}
.flyout-menu a:hover {
background: #ed1e26;
color: white;
}
/* Flyout Overlay */
.flyout-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1001;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.flyout-overlay.open {
opacity: 1;
visibility: visible;
}
/* Responsive Design */
@media (max-width: 768px) {
.header-inner {
padding: 0 15px;
}
.site-logo {
height: 50px;
}
.flyout-menu {
width: 280px;
}
.flyout-menu a {
padding: 12px 15px;
font-size: 14px;
}
}
@media (max-width: 480px) {
.flyout-menu {
width: 100%;
}
.flyout-header {
padding: 15px;
}
.flyout-menu a {
padding: 15px 20px;
font-size: 16px;
}
}
/* Body padding to account for fixed header */
body {
/* padding-top: 80px; */
}
/* Starz overlay styles */
.starz-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
pointer-events: none;
}
.starz-rotating-img {
width: 100%;
height: 100%;
background-image: url('/elements/Starz.png');
background-size: cover;
background-position: center;
animation: rotate 60s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Section container adjustments */
.section-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
position: relative;
z-index: 1;
}
/* Slider section for master shield */
.section-container.slider {
height: 46vw;
z-index: 998;
margin-bottom: 20px;
}
/* Event box styles */
.event-box {
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
overflow: hidden;
}
.poster-box {
color: white;
border-image: url(/elements/star-red-trans.png);
border-image-repeat: repeat;
position: relative;
overflow: hidden;
}
.poster-inner {
padding: 30px;
text-align: center;
}
/* New Two-Column Event Layout */
.event-layout {
display: flex;
gap: 40px;
align-items: flex-start;
max-width: 1000px;
margin: 0 auto;
justify-content: space-between;
flex-wrap: wrap;
flex-direction: row;
width: 100%;
}
.event-poster-column {
flex: 0 0 420px;
}
.event-poster {
width: 100%;
height: auto;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.event-details-column {
flex: 1;
text-align: left;
}
.event-header-info {
/* margin-bottom: 30px; */
}
.event-title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0px;
color: #fff;
display: block;
text-shadow: 3px 3px black;
}
.event-venue-full {
font-size: 1.2rem;
color: white;
margin: 0;
font-weight: 500;
}
.event-date-range {
display: flex;
gap: 40px;
margin-bottom: 30px;
}
.date-range-item {
text-align: center;
}
.date-label {
display: block;
font-size: 1rem;
font-weight: 600;
color: #ffcb0e;
margin-bottom: 10px;
text-transform: uppercase;
}
.date-display {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.date-weekday {
font-size: 1.1rem;
font-weight: 600;
color: #ffffff;
}
.date-day {
font-size: 2.5rem;
font-weight: 700;
line-height: 1;
color: #ffcb0e;
}
.date-suffix {
font-size: 1rem;
vertical-align: super;
}
.date-month {
font-size: 1.3rem;
font-weight: 600;
text-transform: uppercase;
color: #ffffff;
}
.event-showtimes {
margin-top: 20px;
}
.showtimes-title {
font-size: 1.5rem;
color: #ffcb0e;
margin-bottom: 20px;
text-align: left;
}
.showtimes-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.event-date-col {
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 20px;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.event-date-heading h3 {
margin: 0 0 10px 0;
font-size: 1.3rem;
color: #ffcb0e;
}
.event-showtime {
margin-top: 15px;
}
.book-now-btn {
display: inline-block;
background: #ffcb0e;
color: #1d3f78;
text-decoration: none;
padding: 12px 25px;
border-radius: 25px;
font-weight: 700;
font-size: 1.1rem;
transition: all 0.3s ease;
text-transform: uppercase;
}
.book-now-btn:hover {
background: #1d3f78;
color: white;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
/* Master shield */
.master-shield {
/* position: relative; */
/* top: 40px; */
/* left: 50%; */
/* transform: translate(-50%, -50%); */
max-width: 100%;
/* height: auto; */
/* opacity: 1; */
/* z-index: 999; */
/* pointer-events: none; */
}
/* Desktop-only styles */
@media (min-width: 769px) {
.mobile-only {
display: none;
}
.desktop-only {
display: block;
}
}
/* Desc styles */
.desc {
text-align: center;
margin-bottom: 20px;
}
.desc h1 {
margin-bottom: 10px;
}
.desc strong {
font-weight: 600;
color: #ffcb0e;
}
/* Gemini-style showtime layout */
.showtime-date-row {
margin-bottom: 24px;
}
.showtime-date-label {
font-weight: 100;
font-size: 1.3rem;
margin-bottom: 10px;
color: #fff;
}
.showtime-btn-row {
display: flex;
gap: 20px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.showtime-btn {
background: #ffcb0e;
color: #fff;
border: none;
border-radius: 30px;
padding: 12px 20px;
font-size: 1.1rem;
font-weight: bold;
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transition: all .1s ease-in-out;
text-shadow: 2px 2px #000000;
}
.showtime-btn:hover {
background: #ff00bb;
color: white;
transform: scale(1.1);
}
.showtime-time {
font-weight: bold;
font-size: 1.1em;
margin-right: 6px;
}
.showtime-book {
font-size: 1em;
}
.showtime-arrow {
font-size: 1.3em;
margin-left: 6px;
}
/* Showtime status styles */
.showtime-btn.sold-out,
.showtime-btn.cancelled,
.showtime-btn.postponed,
.showtime-btn.booking-closed {
pointer-events: none;
text-decoration: none;
display: flex;
align-items: center;
gap: 10px;
border-radius: 30px;
padding: 12px 28px;
font-size: 1.1rem;
font-weight: bold;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transition: opacity 0.2s;
}
.showtime-btn.sold-out:hover,
.showtime-btn.cancelled:hover,
.showtime-btn.postponed:hover,
.showtime-btn.booking-closed:hover {
opacity: 0.9 !important;
transform: none;
}
@media (max-width: 768px) {
.showtime-btn-row {
flex-direction: column;
gap: 12px;
}
}
this is the current css calling
### 1) Don’t constrain the Stripe container height
Right now you set a fixed height on the payment wrapper:
#event-order .section-payment #payment-fields { ... height: 3em; padding: 1em; ... }
That combination can clip Stripe’s iframe or make the tap area weird inside WebViews.
**Fix**
/* Let the Stripe mount size itself and be tappable */
#event-order .section-payment #payment-fields {
height: auto !important;
min-height: 56px !important; /* 52–56px is safe */
padding: 12px !important;
}
### 2) Give the Elements mount a high z-index and its own stacking context
You have multiple high-z elements (header 999, flyout 1001–1002). If any of them is “open” or overlapping, taps won’t reach the iframe.
**Fix**
/* Stripe Elements + PRB button should sit on top */
.section-payment {
position: relative !important;
z-index: 2000 !important;
}
#card-element,
#payment-request-button,
.ElementsApp,
.InputElement,
#payment-fields {
position: relative !important;
z-index: 2001 !important;
min-height: 56px !important;
}
### 3) Make sure the flyout never covers the payment step
Your flyout/overlay are `z-index: 1002/1001`. If “open” stays on by mistake, it can block touches even if partly off-screen.
**Safety rule during payment step**
/* When you’re on the payment step, disable flyout UI completely */
body.payment-step .flyout-menu,
body.payment-step .flyout-overlay {
display: none !important;
}
(When you enter Step 3 in JS, add `document.body.classList.add('payment-step')`; remove it when leaving.)
### 4) Kill transforms/filters near the mount
iOS/FB WebViews can lose focus inside iframes when any ancestor has `transform`, `filter`, or `opacity < 1`.
You have a rotating background (`.starz-rotating-img`) but it’s behind (`z-index:-1`)—good. Still, neutralize transforms **around** the card element:
/* Temporary guardrail for webviews */
#payment-fields * {
transform: none !important;
filter: none !important;
opacity: 1 !important;
}
### 5) Don’t center/line‑height hack inputs globally
You globally set:
#event-order button, #event-order input, #event-order select, #event-order textarea {
line-height: 3em; height: 3em; text-align: center; ...
}
Stripe’s fields are inside an **iframe** (not your inputs), but the fixed line‑height/height can still create awkward containers.
**Narrow the rule so it excludes the Stripe mount**
/* Keep your styling, but don’t affect the Stripe container */
#event-order button,
#event-order select,
#event-order textarea,
#event-order input:not([id="card-element"]) {
line-height: 3em;
height: 3em;
text-align: center;
}
### 6) Quick JS “focus nudge” (helps FB IAB)
<script>
// after you mount:
// const card = elements.create('card', {...}); card.mount('#card-element');
const mount = document.getElementById('card-element');
if (mount) {
mount.addEventListener('click', () => { try { card.focus(); } catch(_){} });
setTimeout(() => { try { card.focus(); } catch(_){} }, 600);
}
</script>
### 7) One‑minute overlay test (to confirm z-index issue)
Temporarily color the overlay and re-test taps:
.flyout-overlay { background: rgba(255,0,0,.25) !important; }
If the card becomes untappable only when the flyout has `.open`, that’s your blocker.
—
#### Recap (most likely culprits in your CSS)
– Fixed `height: 3em` on `#payment-fields` → **change to min-height**.
– High‑z overlay/flyout (1001/1002) lingering → **hide on payment step**.
– Missing high z-index on the Stripe mount → **set 2000+**.
– Ancestor transforms/filters → **neutralize near the mount**.
Apply the snippet above and try again in the FB in‑app browser. If it’s still sticky, paste your **Stripe Elements mount HTML** (the `#payment-fields` / `#card-element` DOM) and the JS where you toggle the flyout classes—I'll tailor the exact selectors.
1) **Mounted while hidden (display:none) or inside an off‑screen step**, then shown later.
2) **An overlay/stacking/transform issue** that FB’s WebView is extra-picky about.
Here’s a concrete, do‑this‑now plan that usually fixes it:
—
## A) Mount the card **only when the payment step is visible**
If you mount Elements while the step is hidden (e.g. Step 3 is `display:none` until user reaches it), WebViews often break focus/size.
**Patch your step logic:**
<div id="payment-step" style="display:none">
<div id="payment-fields">
<div id="card-element"></div>
<div id="card-errors" role="alert"></div>
</div>
</div>
let stripe, elements, card;
function mountCard() {
if (card) return; // already mounted
if (!stripe) stripe = Stripe('pk_live_or_test'); // no apiVersion here
if (!elements) elements = stripe.elements();
card = elements.create('card', { hidePostalCode: true });
card.mount('#card-element');
// Nudge focus for FB IAB
setTimeout(() => { try { card.focus(); } catch(e){} }, 500);
}
function unmountCard() {
try { if (card) { card.unmount(); card = null; } } catch(e){}
}
function goToStep(step) {
// ...your step switching code...
const payStep = document.getElementById('payment-step');
if (step === 'payment') {
// Make it visible first, THEN mount
payStep.style.display = 'block';
// If you use classes/tailwind, ensure no ancestor is display:none
requestAnimationFrame(() => mountCard());
} else {
// Optional: unmount when leaving payment
unmountCard();
payStep.style.display = 'none';
}
}
> If you cannot change your step system, switch the hidden state from `display:none` to `visibility:hidden; height:auto;` until you mount, then set `visibility:visible`.
—
## B) Apply a **minimal, webview‑safe CSS override** just for the payment area
Paste **at the very end** of your stylesheet:
/* --- FB/IG WebView payment hardening --- */
.section-payment,
#payment-fields,
#card-element {
position: relative !important;
z-index: 3000 !important;
}
#payment-fields {
height: auto !important;
min-height: 56px !important;
padding: 12px !important;
background: #fff !important;
}
/* Don’t apply your global input height/line-height hacks to the Stripe container */
#event-order button,
#event-order select,
#event-order textarea,
#event-order input:not([id="card-element"]) {
line-height: 3em;
height: 3em;
text-align: center;
}
/* Neutralize known focus killers near the mount */
#payment-fields * {
transform: none !important;
filter: none !important;
opacity: 1 !important;
pointer-events: auto !important;
}
/* Ensure overlays never block the payment area */
body.payment-step .flyout-menu,
body.payment-step .flyout-overlay {
display: none !important;
}
And when you enter the payment step in JS:
document.body.classList.add('payment-step');
(remove it when leaving payment)
—
## C) One‑file **minimal test** to isolate the issue
Create `/iab-test.html` (no theme CSS, no headers, nothing else). Open **from a Facebook link** so it loads inside the in‑app browser. If this works, the bug is in your page’s step/overlay CSS; if it fails, your headers/CDN still differ in IAB.
<!doctype html>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: system-ui, sans-serif; padding: 20px; }
#wrap { max-width: 420px; margin: 0 auto; }
#card-element { background:#fff; padding:14px; border-radius:8px; min-height:56px; box-shadow:0 0 0 1px #ddd; }
#card-element, #wrap { position:relative; z-index:10; }
</style>
<div id="wrap">
<h1>Stripe IAB Focus Test</h1>
<div id="card-element"></div>
<button id="pay" style="margin-top:16px;">Test Focus</button>
</div>
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('pk_test_123'); // use your real key
const elements = stripe.elements();
const card = elements.create('card');
card.mount('#card-element');
document.getElementById('card-element').addEventListener('click', () => { try { card.focus(); } catch(e){} });
document.getElementById('pay').addEventListener('click', () => { try { card.focus(); } catch(e){} });
</script>
– **If this page focuses fine in FB IAB:** your main page needs A & B above (mount-after-visible + overlay/height fixes).
– **If this page still doesn’t focus:** your IAB path likely isn’t receiving the CSP header you think it is. Verify with a `/debug/headers` endpoint opened **inside** FB IAB (not desktop) and confirm you see your consolidated `Content-Security-Policy` header there.
—
## D) Safety net (keeps conversion up)
Add an FB/IG in‑app banner to open in the device browser if focusing fails (after a short timeout):
const isIAB = /bFBAN|FBAV|FB_IAB|Instagramb/i.test(navigator.userAgent||'');
setTimeout(() => {
const focused = document.activeElement && document.activeElement.tagName === 'IFRAME';
if (isIAB && !focused) {
// show a small banner/toast with a button:
// “For a secure checkout, open in your browser”
// On tap: window.open(location.href, '_blank'); // or instruct ••• > Open in Browser
}
}, 1500);
—
### Why this works
– Mounting while hidden is the #1 culprit in WebViews (FB/IG/Safari). Mount **after** it’s visible or remount when shown.
– Fixed 3em height containers around iframes + overlays with high z‑index can intercept taps even when you don’t see them.
– The hardening CSS isolates the payment area from the rest of the page’s transforms/overlays.
– The minimal test page quickly tells you if it’s **your page CSS/step code** vs **headers/CDN**.
If you paste your **step switcher JS** (where you show/hide `.section-payment`) and the **HTML around `#payment-fields` / `#card-element`**, I’ll drop in an exact patch wired to your code.
<script>
</script>
<script>
</script>
<script>
function setCookie(name, value, days) {
var expires = "";
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + encodeURIComponent(value || "") + expires + "; path=/";
}
function getCookie(name) {
var match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
return match ? decodeURIComponent(match[2]) : '';
}
// Prevent const/let errors in GTM
try {
var utmParams = new URLSearchParams(window.location.search);
var utm_source = utmParams.get('utm_source') || getCookie('utm_source');
var utm_medium = utmParams.get('utm_medium') || getCookie('utm_medium');
var utm_campaign = utmParams.get('utm_campaign') || getCookie('utm_campaign');
// Only set new cookies if values are in URL
if (utmParams.get('utm_source')) setCookie('utm_source', utm_source, 30);
if (utmParams.get('utm_medium')) setCookie('utm_medium', utm_medium, 30);
if (utmParams.get('utm_campaign')) setCookie('utm_campaign', utm_campaign, 30);
// Push to GTM dataLayer
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'utmParamsLoaded',
utm_source: utm_source,
utm_medium: utm_medium,
utm_campaign: utm_campaign
});
} catch (e) {
console.error("UTM error:", e);
}
</script>
<script>
// Prevent zooming on iOS Safari - only block zoom gestures
document.addEventListener('gesturestart', function(e) {
e.preventDefault();
});
document.addEventListener('gesturechange', function(e) {
e.preventDefault();
});
document.addEventListener('gestureend', function(e) {
e.preventDefault();
});
// Prevent double-tap zoom - but allow normal taps
let lastTouchEnd = 0;
document.addEventListener('touchend', function(event) {
const now = (new Date()).getTime();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
}, false);
// Prevent pinch zoom - but allow normal scrolling
document.addEventListener('touchmove', function(event) {
if (event.touches.length > 1) {
event.preventDefault();
}
}, { passive: false });
// Force viewport scale on load and resize
function setViewportScale() {
const viewport = document.querySelector('meta[name=viewport]');
if (viewport) {
viewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no');
}
}
// Set on load
window.addEventListener('load', setViewportScale);
// Set on orientation change
window.addEventListener('orientationchange', function() {
setTimeout(setViewportScale, 100);
});
// Set on resize
window.addEventListener('resize', setViewportScale);
</script>
View Order
€0.00
Order Summary
Select Tickets
Wheelchair access required?
–
+
ORDER INFORMATION
Add-ons
Pre-Order Your Drinks & Snacks!
Order your refreshments in advance and collect them on arrival, hassle-free. Fast-track your pickup and head straight to your seat – no waiting, just enjoying the show!
Simply present your QR code to a staff member at the collection point or shop
Personal Details & Payment
Order Summary
Total:
€0.00
Have a Voucher?
Enter your voucher code here to redeem the amount off your tickets
Apply Voucher
Whoops! That code isn't right
Voucher valid!
Personal Information
Payment Details
— or pay with card —
By placing this order I agree to the Terms and Conditions & allow Fossetts Circus process my details.
Tickets are non refundable or transferable
All children must be accompanied by an adult
Fossetts reserves the right to refuse entry to anyone
Fossetts reserves the right to change the show times and dates
Buy Now
Please fill in all required fields
<script>
</script>
<script>
function getCookie(name) {
let match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : '';
}
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('utm_source_field').value = getCookie('utm_source');
document.getElementById('utm_medium_field').value = getCookie('utm_medium');
document.getElementById('utm_campaign_field').value = getCookie('utm_campaign');
});
</script>
this buy-ticket.php where card inputs showjng
1) You’re globally **blocking touch events/zoom** (the big script in “ with `gesture*` + `touchend` + `touchmove preventDefault`). This is notorious for killing focus inside iframes (Stripe Elements lives in an iframe).
2) Stripe Elements may be **mounted before Step 3 is visible**, which webviews hate.
Do these **surgical fixes** (copy‑paste):
—
## 1) Remove the global “prevent zoom” script
Delete this whole block from “ (or comment it out):
<!-- REMOVE this block -->
<script>
document.addEventListener('gesturestart', function(e){ e.preventDefault(); });
document.addEventListener('gesturechange', function(e){ e.preventDefault(); });
document.addEventListener('gestureend', function(e){ e.preventDefault(); });
let lastTouchEnd = 0;
document.addEventListener('touchend', function(event){
const now = (new Date()).getTime();
if (now - lastTouchEnd <= 300) { event.preventDefault(); }
lastTouchEnd = now;
}, false);
document.addEventListener('touchmove', function(event){
if (event.touches.length > 1) { event.preventDefault(); }
}, { passive: false });
function setViewportScale(){ /* ... */ }
window.addEventListener('load', setViewportScale);
window.addEventListener('orientationchange', function(){ setTimeout(setViewportScale, 100); });
window.addEventListener('resize', setViewportScale);
</script>
**Why:** `preventDefault()` on `touchend`/`touchmove` at the **document** level often blocks focus transfer to iframes in FB/IG WebViews.
Keep a simple viewport tag instead (you already have one):
<meta name="viewport" content="width=device-width, initial-scale=1.0">
—
## 2) Remove the meta‑CSP line
In “ you still have:
<meta http-equiv="Content-Security-Policy" content="frame-ancestors 'self' https://js.stripe.com;">
Remove it. Serve CSP **via header only** (we already set that earlier). Meta CSP with only `frame-ancestors` is ignored by many browsers and can cause weird interactions in IABs.
—
## 3) Mount Stripe only when Step 3 is visible (and remount if user leaves/returns)
Given your HTML uses split fields (`#card-number`, `#card-expiry`, `#card-cvc`), wire this into your step code (add this near the bottom, after `buy-tickets.js` loads—or inside it):
<script>
let stripe, elements, cardNumber, cardExpiry, cardCvc;
function ensureStripe() {
if (!stripe) stripe = Stripe('pk_live_or_test'); // no apiVersion here
if (!elements) elements = stripe.elements();
}
function mountCardIfNeeded() {
if (cardNumber) return; // already mounted
ensureStripe();
cardNumber = elements.create('cardNumber', { showIcon: true });
cardExpiry = elements.create('cardExpiry');
cardCvc = elements.create('cardCvc');
cardNumber.mount('#card-number');
cardExpiry.mount('#card-expiry');
cardCvc.mount('#card-cvc');
// Nudge focus for FB IAB
setTimeout(() => { try { cardNumber.focus(); } catch(e){} }, 400);
}
function unmountCard() {
try { if (cardNumber) { cardNumber.unmount(); cardNumber = null; } } catch(e){}
try { if (cardExpiry) { cardExpiry.unmount(); cardExpiry = null; } } catch(e){}
try { if (cardCvc) { cardCvc.unmount(); cardCvc = null; } } catch(e){}
}
/** Call this when you navigate steps */
function goToStep(stepIndex) {
// your existing step show/hide code…
const step3 = document.getElementById('step3');
if (stepIndex === 3) {
// Make step 3 VISIBLE first, then mount:
// If you use classes, ensure they do NOT set display:none on step3 ancestors
requestAnimationFrame(() => {
mountCardIfNeeded();
document.body.classList.add('payment-step'); // for CSS hardening
});
} else {
unmountCard();
document.body.classList.remove('payment-step');
}
}
/* If your existing code uses next/prev buttons,
call goToStep(3) when entering the "Personal Details & Payment" step. */
</script>
—
## 4) Add tiny CSS hardening near the payment fields
Right before “ (or at the end of your theme CSS), add:
<style>
/* Keep Stripe’s iframes tappable and above overlays */
.payment-section, #card-number, #card-expiry, #card-cvc {
position: relative !important;
z-index: 3000 !important;
}
.payment-fields {
background: #fff;
padding: 12px;
border-radius: 8px;
box-shadow: 0 0 0 1px rgba(0,0,0,.1);
}
/* Don’t let global input sizing affect Stripe iframes */
#event-order input:not(#card-number):not(#card-expiry):not(#card-cvc) {
line-height: 3em;
height: 3em;
text-align: center;
}
/* No transforms/filters around payment */
.payment-section * {
transform: none !important;
filter: none !important;
opacity: 1 !important;
pointer-events: auto !important;
}
/* Hide any flyout/overlay while paying */
body.payment-step .flyout-menu,
body.payment-step .flyout-overlay {
display: none !important;
}
</style>
—
## 5) Quick sanity checks
– Stripe init must be `Stripe('pk_…')` **without** `{ apiVersion: … }`.
– Your **server header CSP** (not meta) includes at least:
– `script-src https://js.stripe.com https://pay.google.com https://www.gstatic.com https://*.gstatic.com`
– `frame-src https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://payments.google.com`
– `connect-src https://api.stripe.com https://r.stripe.com https://m.stripe.network https://m.stripe.com`
– Step 3 (or any ancestor of `#card-number/#card-expiry/#card-cvc`) is **not `display:none`** when you call `mountCardIfNeeded()`.
—
## 6) If you still see no focus in FB IAB
Create a minimal test page (no theme, no anti‑zoom, no flyout) and open it from a Facebook post. If that works, the issue is 100% from page scripts/CSS; if not, your CSP header may not reach the IAB path—verify with a simple `/debug/headers` endpoint opened **inside** the FB browser.
—
These changes (especially **removing the global touch blockers** and **mounting after visible**) usually flip FB in‑app from “shows but not tappable” → “works”. If you want, paste your current **step navigation function** (the code that moves from `.step_2` → `.step_3`) and I’ll wire the `goToStep(3)` / mount calls exactly where they belong.
Below is a tiny, safe **diagnostic kit** you can paste **near the end of your page** (before “). It will:
– Detect any **overlay/element covering** the Stripe fields
– Confirm whether **Step 3 is hidden** when Elements mounts
– Temporarily **neutralize sticky bars / slide‑outs / overlays / touch blockers**
– Let you **remount** Elements after the step is visible
– Optionally swap to a **single Card Element** to rule out split‑fields CSS
> You don’t have to keep this code—use it to find the culprit, then remove it.
—
### 1) Paste this diagnostic JS (below your `buy-tickets.js` include)
<script>
/* ===== 0) QUICK KILL‑SWITCHES (TEMP) ===== */
/* Kill obvious overlays while testing */
(function hardenUI(){
const css = `
/* hide anything that could overlap */
.sticky-cart-bar, .cart-slide-out, .flyout-menu, .flyout-overlay { display: none !important; }
/* put payment area on top */
.payment-section, #card-number, #card-expiry, #card-cvc { position: relative !important; z-index: 5000 !important; }
.payment-fields { min-height: 56px !important; padding: 12px !important; background:#fff !important; }
/* neutralize transforms/filters near the mount */
.payment-section * { transform:none !important; filter:none !important; opacity:1 !important; pointer-events:auto !important; }
/* avoid global input height hacks touching Stripe iframes */
#event-order input:not(#card-number):not(#card-expiry):not(#card-cvc) { line-height: 3em; height: 3em; text-align:center; }
`;
const s = document.createElement('style'); s.textContent = css; document.documentElement.appendChild(s);
})();
/* ===== 1) STEP VISIBILITY + (RE)MOUNT ===== */
let stripe, elements, cardNumber, cardExpiry, cardCvc;
function ensureStripe() {
if (!stripe) stripe = Stripe('pk_live_or_test'); // <- use your real key. NO apiVersion here.
if (!elements) elements = stripe.elements();
}
function mountSplit() {
if (cardNumber) return;
ensureStripe();
cardNumber = elements.create('cardNumber', { showIcon: true });
cardExpiry = elements.create('cardExpiry');
cardCvc = elements.create('cardCvc');
cardNumber.mount('#card-number');
cardExpiry.mount('#card-expiry');
cardCvc.mount('#card-cvc');
setTimeout(() => { try { cardNumber.focus(); } catch(e){} }, 400);
}
function unmountSplit() {
try { cardNumber && cardNumber.unmount(); } catch(e){}
try { cardExpiry && cardExpiry.unmount(); } catch(e){}
try { cardCvc && cardCvc.unmount(); } catch(e){}
cardNumber = cardExpiry = cardCvc = null;
}
/* Re-mount AFTER step3 is actually visible */
function ensureVisibleThenMount() {
const step3 = document.getElementById('step3');
if (!step3) return;
const hidden = window.getComputedStyle(step3).display === 'none' || step3.offsetParent === null;
if (hidden) {
console.warn('[diag] step3 hidden at mount time. Forcing visible then mounting.');
step3.style.display = 'block';
step3.style.visibility = 'visible';
}
requestAnimationFrame(() => { unmountSplit(); mountSplit(); });
}
/* Run once when DOM ready */
document.addEventListener('DOMContentLoaded', () => {
// If your own code already mounts, we remount safely after a tick.
setTimeout(ensureVisibleThenMount, 300);
// Add a button to re-run diagnostics (appears only for you)
const btn = document.createElement('button');
btn.textContent = '🔎 Run Stripe tap test';
Object.assign(btn.style, {position:'fixed',bottom:'12px',right:'12px',zIndex:6000});
btn.onclick = () => { runHitTest(); };
document.body.appendChild(btn);
});
/* ===== 2) HIT‑TEST: who is on top of the Stripe area? ===== */
function runHitTest() {
const targets = ['#card-number', '#card-expiry', '#card-cvc'];
const results = [];
targets.forEach(sel => {
const el = document.querySelector(sel);
if (!el) { results.push({sel, err:'not found'}); return; }
const r = el.getBoundingClientRect();
const pts = [
[r.left+5, r.top+5], [r.right-5, r.top+5],
[r.left+5, r.bottom-5], [r.right-5, r.bottom-5],
[r.left + r.width/2, r.top + r.height/2]
];
pts.forEach(([x,y]) => {
const top = document.elementFromPoint(x,y);
results.push({ sel, x:Math.round(x), y:Math.round(y), top: summarize(top) });
});
});
console.table(results);
// Visual overlay for a second
const overlay = document.createElement('div');
Object.assign(overlay.style, { position:'fixed', left:0, top:0, right:0, bottom:0, background:'rgba(0,128,255,.08)', zIndex:5999, pointerEvents:'none' });
document.body.appendChild(overlay);
setTimeout(()=> overlay.remove(), 1000);
function summarize(node){
if (!node) return '(none)';
const path = [];
let n=node, c=0;
while (n && n !== document.documentElement && c<5) {
path.push(n.tagName.toLowerCase() + (n.id?('#'+n.id):'') + (n.className?('.'+String(n.className).trim().replace(/s+/g,'.')):''));
n = n.parentElement; c++;
}
return path.join(' <- ');
}
}
/* ===== 3) OPTIONAL: swap to single Card Element to rule out split‑CSS ===== */
// Call swapToSingleCard() from console if split still won’t focus.
function swapToSingleCard() {
unmountSplit();
const wrap = document.querySelector('.payment-fields');
if (!wrap) return alert('payment-fields not found');
let single = document.getElementById('card-single');
if (!single) {
single = document.createElement('div');
single.id = 'card-single';
single.style.cssText = 'min-height:56px;padding:12px;background:#fff;border-radius:8px;box-shadow:0 0 0 1px #ddd;margin-top:10px;';
wrap.appendChild(single);
}
ensureStripe();
const card = elements.create('card', { hidePostalCode: true });
card.mount('#card-single');
setTimeout(()=> { try { card.focus(); } catch(e){} }, 400);
console.warn('[diag] Using single Card Element (temporary). If this works, CSS around split fields is the issue.');
}
</script>
—
### 2) What you should see / do
1) **Open inside Facebook** and click the **“🔎 Run Stripe tap test”** button.
– In the console you’ll get a table of the **topmost element** at multiple points of each Stripe field.
– If you see `.flyout-overlay`, `.sticky-cart-bar`, `.cart-slide-out`, `.site-header`, etc. at those points → that element is blocking taps. Remove/adjust it (or keep the test’s “hide” rules and move the real fix into your CSS).
2) If the hit‑test shows an **`iframe` / `.InputElement`** on top but still no focus, run:
– `swapToSingleCard()` in the console.
If the single **Card Element** focuses fine, the issue is **split‑field container CSS** (fixed heights, grid gaps, transforms). Keep single Card or adjust split containers.
3) If the test still fails:
– Make sure you **removed the anti‑zoom script** (any global `touchend`/`touchmove` with `preventDefault` in your codebase—not just this file).
– Confirm **no meta CSP** is left. CSP must be the **header** we set earlier.
– Double‑check that **`goToStep(3)`** (or your step logic) doesn’t keep `#step3` or any ancestor as `display:none` when mounting. The diagnostic already forces visibility and remounts; if that fixes it, wire the same behavior into your real step code.
—
### Likely outcomes & fixes
– **Overlay found on top** (very common with sticky bars/slide‑outs):
Lower its `z-index`, limit its scope, or hide it on the payment step.
You can keep:
body.payment-step .sticky-cart-bar,
body.payment-step .cart-slide-out,
body.payment-step .flyout-menu,
body.payment-step .flyout-overlay { display:none !important; }
and toggle `document.body.classList.toggle('payment-step', stepIndex === 3);`
– **Split fields only fail, single Card works:**
Reduce constraints on `.payment-fields` and each field’s container; avoid fixed heights; keep `min-height:56px; padding:12px;` and no transforms.
– **Still failing even with the diag kit:**
Then your **header CSP isn’t reaching** the in‑app path or something else is intercepting. Visit a minimal page (e.g., `/iab-test.html`) with just Stripe and log **response headers** inside FB IAB (use a quick `/debug/headers` endpoint). If CSP header is missing there, fix the CDN/origin rule for IAB traffic.
—
If you run the **hit‑test** and paste the console table (top element paths) back, I’ll point to the exact selector to change.