Case
Process the payment first and then request the user’s email, establishing the linkage between the transaction and the user account afterward.
Solution
Use custom analytics to save the userEmail
into window.customData
from localStorage
(under the key oo-user-email
) at the moment of a successful purchase.
Example of Analytics
<script> window.customEventTracker = (event, args) => { const successPaymentEvents = [ 'StripePurchase', 'StripeStartTrial', 'StripeSubscribe', 'PaddleSubscribe', 'PaddlePurchase', 'PaddleStartTrial', ]; if (successPaymentEvents.includes(event) && localStorage.getItem('oo-user-email')) { const userEmail = localStorage.getItem('oo-user-email'); window.customData ??= {}; window.customData.userEmail = userEmail; } }; </script>
Example of a Custom Screen
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8' />
<title>Some custom screen</title>
<!-- OO-PORT-MESSAGE-HANDLER-START -->
<!-- DO NOT REMOVE THIS CODE -->
<script>
window.onboardingOnlineCustomScreenData = {
inputs: {},
labels: {},
userData: {},
customData: {},
};
window.nextScreen = () => {};
window.backScreen = () => {};
window.acceptCookie = () => {};
window.declineCookie = () => {};
function renderEmailBadge() {
const email = (window.onboardingOnlineCustomScreenData?.customData || {}).userEmail;
const emailEl = document.getElementById('user-email');
const avatarEl = document.getElementById('user-avatar');
const container = document.getElementById('email-card');
if (!container) return; // safety
if (email) {
emailEl.textContent = email;
const initial = (email.trim()[0] || '?').toUpperCase();
avatarEl.textContent = initial;
container.classList.remove('hidden');
} else {
// If userEmail is absent, keep the card hidden (no layout shift)
container.classList.add('hidden');
}
}
window.onmessage = (event) => {
if (typeof event.data === 'object' && event.data.eventName === 'InitCustomScreen') {
window.onboardingOnlineCustomScreenData.inputs = event.data.args.inputs;
window.onboardingOnlineCustomScreenData.labels = event.data.args.labels;
window.onboardingOnlineCustomScreenData.userData = event.data.args.userData;
window.onboardingOnlineCustomScreenData.customData = event.data.args.customData;
// Render the email (if provided) right after we receive data
try { renderEmailBadge(); } catch (e) { /* no-op */ }
const port = event.ports[0];
if (port) {
window.nextScreen = (args) => {
port.postMessage(
{
eventName: 'NextScreen',
args,
},
[]
);
};
window.backScreen = () => {
port.postMessage(
{
eventName: 'BackScreen',
args: null,
},
[]
);
};
window.acceptCookie = () => {
port.postMessage(
{
eventName: 'AcceptCookie',
args: null,
},
[]
);
};
window.declineCookie = () => {
port.postMessage(
{
eventName: 'DeclineCookie',
args: null,
},
[]
);
};
}
}
};
</script>
<!-- OO-PORT-MESSAGE-HANDLER-END -->
<style>
:root {
--card-bg: #0f172a0d;
--border: #e2e8f0;
--text: #0f172a;
--muted: #64748b;
--accent: #2563eb;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
color: var(--text);
background-color: #ffffff;
}
.page {
min-height: 100vh;
display: grid;
place-items: center;
padding: 24px;
}
.stack { display: grid; gap: 16px; }
/* Email card */
.card {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 16px;
backdrop-filter: blur(4px);
}
.avatar {
width: 36px;
height: 36px;
border-radius: 999px;
display: grid;
place-items: center;
font-weight: 700;
border: 1px solid var(--border);
background: #fff;
}
.meta { line-height: 1.25; }
.label { font-size: 12px; color: var(--muted); }
.value { font-size: 14px; font-weight: 600; word-break: break-all; }
.hidden { display: none; }
/* Form */
#custom-form {
display: grid;
gap: 16px;
justify-items: center;
}
.btn {
appearance: none;
border: 1px solid var(--border);
background: #fff;
padding: 10px 16px;
border-radius: 12px;
font-weight: 600;
cursor: pointer;
transition: transform .05s ease, box-shadow .2s ease;
box-shadow: 0 1px 0 0 rgba(2,6,23,.05);
}
.btn:active { transform: translateY(1px); }
.btn-primary {
border-color: transparent;
background: var(--accent);
color: #fff;
box-shadow: 0 6px 14px rgba(37,99,235,.25);
}
</style>
</head>
<body>
<main class="page">
<div class="stack">
<!-- Email badge card (auto-hides when userEmail is absent) -->
<div id="email-card" class="card hidden">
<div id="user-avatar" class="avatar" aria-hidden="true">?</div>
<div class="meta">
<div class="label">Signed in as</div>
<div id="user-email" class="value" aria-live="polite"></div>
</div>
</div>
<form id='custom-form'>
<button id='submit-button' class="btn btn-primary" type="submit">Next screen</button>
</form>
</div>
</main>
<script>
const form = document.getElementById('custom-form');
form.addEventListener('submit', function (event) {
event.preventDefault();
window.nextScreen({ someField: 'someValue' });
});
</script>
</body>
</html>
🔗 Example: Custom screen demo
Important note:
If you are using Stripe PaymentElement or ExpressCheckout where email isn't required - make sure to include a funnel step that collects the email. This ensures
PlatformUser
andStripeCustomer
are updated, and subscriptions can be found by this Email.For Paddle the email is always required.