{"id":2168,"date":"2026-02-13T10:51:59","date_gmt":"2026-02-13T09:51:59","guid":{"rendered":"https:\/\/oceanbeat.com\/?page_id=2168"},"modified":"2026-05-13T11:36:16","modified_gmt":"2026-05-13T09:36:16","slug":"oceanbeat-tickets","status":"publish","type":"page","link":"https:\/\/oceanbeat.com\/fr\/oceanbeat-tickets\/","title":{"rendered":"Oceanbeat Tickets"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"2168\" class=\"elementor elementor-2168\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-1f70353 e-con-full e-flex e-con e-parent\" data-id=\"1f70353\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-303aa5a elementor-widget elementor-widget-html\" data-id=\"303aa5a\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<div class=\"oceanbeat-seamless-embed\" style=\"width: 100%; margin: 0; padding: 0; max-width: none;\">\n  <iframe\n    id=\"oceanbeat-booking-iframe\"\n    src=\"\"\n    title=\"Book your boat party \u2013 Ocean Beat\"\n    width=\"100%\"\n    height=\"1150\"\n    style=\"display: block; border: none; margin: 0; padding: 0; width: 100%; min-height: 1100px;\"\n    loading=\"eager\"\n  ><\/iframe>\n  <script>\n  (function() {\n    var container = document.querySelector('.oceanbeat-seamless-embed');\n    var iframe = document.getElementById('oceanbeat-booking-iframe');\n    var base = 'https:\/\/oceanbeat-frontend.vercel.app';\n    var apiBase = 'https:\/\/oceanbeat-ticketsystem-production.up.railway.app\/api\/v1';\n    var rawSearch = String(window.location.search || '').replace(\/&amp;\/gi, '&');\n    var params = new URLSearchParams(rawSearch);\n    var bookingNumber = params.get('bookingNumber') || params.get('booking');\n    var token = params.get('token');\n    var isPaymentReturn = params.get('paymentSuccess') === '1' && bookingNumber && token;\n\n    function loadIframe(extraQs) {\n      var returnBase = window.location.origin + window.location.pathname.replace(\/\\\/+$\/, '');\n      var qs = 'returnBase=' + encodeURIComponent(returnBase);\n      if (extraQs) qs += '&' + extraQs;\n      iframe.src = base + '?' + qs;\n    }\n\n    function isPaidStatus(s) {\n      var t = String(s || '').toLowerCase();\n      return t === 'paid_full' || t === 'paid_deposit';\n    }\n\n    if (isPaymentReturn) {\n      \/\/ Mollie redirects to this URL on EVERY outcome (paid, cancelled, failed,\n      \/\/ expired). Show a verifying spinner and only render the success view\n      \/\/ after the backend confirms paid_full\/paid_deposit. Otherwise reload\n      \/\/ the iframe so the user can retry \u2014 never show \"complete and paid\"\n      \/\/ without proof.\n      iframe.style.display = 'none';\n      var verifyEl = document.createElement('div');\n      verifyEl.id = 'ob-verifying';\n      verifyEl.style.cssText = 'font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;max-width:480px;margin:0 auto;padding:48px 16px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:16px;min-height:300px;justify-content:center';\n      verifyEl.innerHTML =\n        '<div style=\"width:48px;height:48px;border:4px solid #e2e8f0;border-top-color:#000;border-radius:50%;animation:ob-spin .8s linear infinite\"><\/div>' +\n        '<p style=\"margin:0;color:#64748b;font-size:14px\">Verifying your payment\u2026<\/p>' +\n        '<style>@keyframes ob-spin{to{transform:rotate(360deg)}}<\/style>';\n      container.appendChild(verifyEl);\n\n      \/\/ Clean URL immediately so a refresh doesn't replay the verify flow.\n      if (window.history && window.history.replaceState) {\n        window.history.replaceState({}, document.title, window.location.pathname);\n      }\n\n      function fallbackToIframe() {\n        if (verifyEl.parentNode) verifyEl.parentNode.removeChild(verifyEl);\n        iframe.style.display = '';\n        \/\/ Pass paymentFailed + bookingNumber + token so the iframe shows the\n        \/\/ \"payment not completed\" banner and reuses the existing booking on retry.\n        var extra = 'paymentFailed=1&bookingNumber=' + encodeURIComponent(bookingNumber);\n        if (token) extra += '&token=' + encodeURIComponent(token);\n        loadIframe(extra);\n      }\n\n      function renderSuccess(b) {\n        if (verifyEl.parentNode) verifyEl.parentNode.removeChild(verifyEl);\n        var el = document.createElement('div');\n        el.id = 'ob-native-success';\n        el.innerHTML =\n          '<div style=\"font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;max-width:480px;margin:0 auto;padding:32px 16px;text-align:center\">' +\n            '<div style=\"width:56px;height:56px;border-radius:50%;background:#10b981;color:#fff;display:flex;align-items:center;justify-content:center;margin:0 auto 16px;font-size:28px;font-weight:bold\">\u2713<\/div>' +\n            '<h2 style=\"margin:0 0 8px;font-size:20px;color:#1e293b\">Your booking is complete and paid.<\/h2>' +\n            '<p style=\"margin:0 0 24px;color:#64748b;font-size:14px\">Enter your email to receive the booking confirmation and tickets.<\/p>' +\n            '<div id=\"ob-email-form\" style=\"display:flex;flex-direction:column;gap:12px;margin-bottom:24px\">' +\n              '<input type=\"email\" id=\"ob-email-input\" placeholder=\"your@email.com\" style=\"padding:12px 16px;border:1px solid #e2e8f0;border-radius:8px;font-size:16px;outline:none;transition:border-color .2s\" \/>' +\n              '<button type=\"button\" id=\"ob-send-btn\" style=\"padding:12px 16px;border:none;border-radius:8px;background:#10b981;color:#fff;font-size:14px;font-weight:600;cursor:pointer;transition:background .2s\">SEND CONFIRMATION & TICKETS<\/button>' +\n            '<\/div>' +\n            '<p id=\"ob-email-msg\" style=\"display:none;margin:0 0 16px;font-size:14px\" aria-live=\"polite\"><\/p>' +\n            '<a href=\"https:\/\/oceanbeat.com\" id=\"ob-back-home\" style=\"display:inline-block;margin-bottom:24px;padding:10px 24px;border:1px solid #e2e8f0;border-radius:8px;color:#1e293b;font-size:14px;font-weight:500;text-decoration:none;transition:background .2s\">&larr; Back to Home<\/a>' +\n            '<div id=\"ob-overview\" style=\"background:#f8fafc;border-radius:12px;padding:16px;text-align:left\">' +\n              '<div style=\"display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #e2e8f0\"><span style=\"color:#64748b;font-size:13px\">Booking Number<\/span><span style=\"color:#1e293b;font-size:13px;font-weight:600\">' + bookingNumber + '<\/span><\/div>' +\n              '<div style=\"display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #e2e8f0\"><span style=\"color:#64748b;font-size:13px\">Event<\/span><span style=\"color:#1e293b;font-size:13px;font-weight:600\">' + ((b.event && b.event.name) || '-') + '<\/span><\/div>' +\n              '<div style=\"display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #e2e8f0\"><span style=\"color:#64748b;font-size:13px\">Date<\/span><span style=\"color:#1e293b;font-size:13px;font-weight:600\">' + ((b.eventDate && (b.eventDate.date || '').slice(0, 10)) || '-') + '<\/span><\/div>' +\n              '<div style=\"display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #e2e8f0\"><span style=\"color:#64748b;font-size:13px\">Time<\/span><span style=\"color:#1e293b;font-size:13px;font-weight:600\">' + ((b.eventDate && (b.eventDate.startTime || '').slice(0, 5)) || '-') + '<\/span><\/div>' +\n              '<div style=\"display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #e2e8f0\"><span style=\"color:#64748b;font-size:13px\">Persons<\/span><span style=\"color:#1e293b;font-size:13px;font-weight:600\">' + (b.numberOfPersons || '-') + '<\/span><\/div>' +\n              '<div style=\"display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #e2e8f0\"><span style=\"color:#64748b;font-size:13px\">Total<\/span><span style=\"color:#1e293b;font-size:13px;font-weight:600\">' + (b.totalPrice != null ? '\u20ac' + parseFloat(b.totalPrice).toFixed(2) : '-') + '<\/span><\/div>' +\n              '<div style=\"display:flex;justify-content:space-between;padding:6px 0\"><span style=\"color:#64748b;font-size:13px\">Status<\/span><span style=\"color:#10b981;font-size:13px;font-weight:600\">' + (b.paymentStatus === 'paid_deposit' ? 'Deposit paid' : 'Fully paid') + '<\/span><\/div>' +\n            '<\/div>' +\n          '<\/div>';\n        container.appendChild(el);\n\n        \/\/ GTM purchase event ONLY on confirmed paid status.\n        window.dataLayer = window.dataLayer || [];\n        window.dataLayer.push({\n          event: 'purchase',\n          ecommerce: {\n            currency: 'EUR',\n            value: b.totalPrice ? parseFloat(b.totalPrice) : undefined,\n            transaction_id: bookingNumber,\n            items: [{\n              item_name: (b.event && b.event.name) || undefined,\n              item_id: (b.event && (b.event.id || b.event.slug)) || undefined,\n              item_variant: b.ticketType || 'Standard',\n              item_category: 'Boat Party',\n              price: b.totalPrice && b.numberOfPersons ? parseFloat((b.totalPrice \/ b.numberOfPersons).toFixed(2)) : undefined,\n              quantity: b.numberOfPersons || 1\n            }]\n          },\n          ob_booking_number: bookingNumber,\n          ob_language: document.documentElement.lang || 'en'\n        });\n\n        var sendBtn = document.getElementById('ob-send-btn');\n        var emailInput = document.getElementById('ob-email-input');\n        var emailMsg = document.getElementById('ob-email-msg');\n        sendBtn.addEventListener('click', function() {\n          var email = (emailInput.value || '').trim();\n          if (!email || email.indexOf('@') === -1) {\n            emailMsg.style.display = 'block';\n            emailMsg.style.color = '#ef4444';\n            emailMsg.textContent = 'Please enter a valid email address.';\n            return;\n          }\n          sendBtn.disabled = true;\n          sendBtn.textContent = 'Sending...';\n          fetch(apiBase + '\/bookings\/' + encodeURIComponent(bookingNumber) + '\/send-confirmation', {\n            method: 'POST',\n            headers: { 'Content-Type': 'application\/json', 'X-Booking-Token': token },\n            body: JSON.stringify({ email: email })\n          })\n          .then(function(r) {\n            if (!r.ok) throw new Error('Failed');\n            emailMsg.style.display = 'block';\n            emailMsg.style.color = '#10b981';\n            emailMsg.textContent = 'Confirmation and tickets sent to ' + email + '!';\n            document.getElementById('ob-email-form').style.display = 'none';\n          })\n          .catch(function() {\n            emailMsg.style.display = 'block';\n            emailMsg.style.color = '#ef4444';\n            emailMsg.textContent = 'Could not send email. Please try again.';\n            sendBtn.disabled = false;\n            sendBtn.textContent = 'SEND CONFIRMATION & TICKETS';\n          });\n        });\n      }\n\n      \/\/ Step 1: trigger backend Mollie poll (handles webhook lag for wallet payments).\n      \/\/ Step 2: fetch the booking and decide based on real status.\n      fetch(apiBase + '\/payments\/check-status', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application\/json', 'X-Booking-Token': token },\n        body: JSON.stringify({ bookingNumber: bookingNumber })\n      })\n      .catch(function() {})\n      .then(function() {\n        return fetch(apiBase + '\/bookings\/' + encodeURIComponent(bookingNumber), {\n          headers: { 'X-Booking-Token': token }\n        });\n      })\n      .then(function(r) {\n        if (!r || !r.ok) throw new Error('booking-fetch-failed');\n        return r.json();\n      })\n      .then(function(data) {\n        var b = data.booking || data;\n        if (isPaidStatus(b && b.paymentStatus)) {\n          renderSuccess(b);\n        } else {\n          fallbackToIframe();\n        }\n      })\n      .catch(function() {\n        \/\/ Network error \u2192 safer to send the user back to checkout than to fake success.\n        fallbackToIframe();\n      });\n      return;\n    }\n\n    \/\/ \u2500\u2500\u2500 Normal booking flow (iframe) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n    loadIframe(params.toString());\n\n    \/\/ Bridge: handle iframe messages (analytics, scroll, resize)\n    window.addEventListener('message', function(e) {\n      if (!e.data) return;\n      if (e.data.type === 'ob-scroll-top') { window.scrollTo({ top: 0, behavior: 'smooth' }); return; }\n      if (e.data.type === 'ob-resize' && e.data.height) { iframe.style.height = e.data.height + 'px'; return; }\n      if (e.data.type !== 'ob-analytics') return;\n      window.dataLayer = window.dataLayer || [];\n      window.dataLayer.push({\n        event: e.data.event,\n        ecommerce: {\n          currency: e.data.currency,\n          value: e.data.value,\n          transaction_id: e.data.transaction_id,\n          coupon: e.data.coupon_code,\n          items: [{\n            item_name: e.data.event_name,\n            item_id: e.data.item_id,\n            item_variant: e.data.ticket_type,\n            item_category: e.data.item_category,\n            price: e.data.price_per_person,\n            quantity: e.data.quantity\n          }]\n        },\n        ob_date: e.data.date,\n        ob_time: e.data.time,\n        ob_booking_number: e.data.booking_number,\n        ob_payment_method: e.data.payment_method,\n        ob_payment_type: e.data.payment_type,\n        ob_language: e.data.language\n      });\n    });\n  })();\n  <\/script>\n<\/div>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"","protected":false},"author":6,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_canvas","meta":{"_seopress_robots_primary_cat":"","_seopress_titles_title":"","_seopress_titles_desc":"","_seopress_robots_index":"yes","footnotes":""},"class_list":["post-2168","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/oceanbeat.com\/fr\/wp-json\/wp\/v2\/pages\/2168","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/oceanbeat.com\/fr\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/oceanbeat.com\/fr\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/oceanbeat.com\/fr\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/oceanbeat.com\/fr\/wp-json\/wp\/v2\/comments?post=2168"}],"version-history":[{"count":47,"href":"https:\/\/oceanbeat.com\/fr\/wp-json\/wp\/v2\/pages\/2168\/revisions"}],"predecessor-version":[{"id":3016,"href":"https:\/\/oceanbeat.com\/fr\/wp-json\/wp\/v2\/pages\/2168\/revisions\/3016"}],"wp:attachment":[{"href":"https:\/\/oceanbeat.com\/fr\/wp-json\/wp\/v2\/media?parent=2168"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}