Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions src/core/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,15 @@ coreRouter.post('/register-session', sessionLimiter, async (req: Request, res: R
const minGatewayBalance = typeof ratePerSecond === 'number' ? ratePerSecond : 0.01;

if (gatewayBalanceNum < minGatewayBalance && walletUsdc < minWalletBalance) {
console.log(`[Core] ⏳ Waiting for ephemeral wallet to receive funds...`);
console.log(`[Core] Waiting for ephemeral wallet to receive funds...`);
let attempts = 0;
while (attempts < 15 && walletUsdc < minWalletBalance) {
await new Promise(resolve => setTimeout(resolve, 2000));
while (attempts < 12 && walletUsdc < minWalletBalance) {
await new Promise(resolve => setTimeout(resolve, 1500));
balances = await gatewayClient.getBalances();
walletUsdc = Number(balances.wallet.formatted);
attempts++;
}
console.log(`[Core] 💰 Final Ephemeral wallet balance: ${walletUsdc} USDC`);
console.log(`[Core] 💰 Final Ephemeral wallet balance: ${walletUsdc} USDC`);
}

// If the user already has enough balance in the Gateway, skip the deposit phase!
Expand All @@ -199,7 +199,7 @@ coreRouter.post('/register-session', sessionLimiter, async (req: Request, res: R
let depositedAmount = '0';

if (gatewayBalanceNum >= minGatewayBalance) {
console.log(`[Core] ⏩ User already has ${gatewayBalanceNum} USDC in Gateway. Skipping deposit phase.`);
console.log(`[Core] User already has ${gatewayBalanceNum} USDC in Gateway. Skipping deposit phase.`);
skippedDeposit = true;
} else {
if (walletUsdc < minWalletBalance) {
Expand All @@ -209,30 +209,30 @@ coreRouter.post('/register-session', sessionLimiter, async (req: Request, res: R
// 3. Deposit to Gateway
const retainedGasAmount = Number(process.env.RETAINED_GAS_AMOUNT || '0.01');
const depositAmount = Math.max(0, walletUsdc - retainedGasAmount).toFixed(2);
console.log(`[Core] 💳 Depositing ${depositAmount} USDC to Circle Gateway...`);
console.log(`[Core] 💳 Depositing ${depositAmount} USDC to Circle Gateway...`);

const depositResult = await gatewayClient.deposit(depositAmount);
depositTxHash = depositResult.depositTxHash;
depositedAmount = depositResult.formattedAmount;

console.log(`[Core] ✅ Deposit confirmed! Tx: ${depositTxHash}`);
console.log(`[Core] Deposit confirmed! Tx: ${depositTxHash}`);

// Wait for deposit to reflect in Gateway balance
console.log(`[Core] ⏳ Waiting for deposit to reflect in Gateway balance...`);
// Wait for deposit to reflect in Gateway balance (max 15s to fit within Nginx proxy timeouts)
console.log(`[Core] Waiting for deposit to reflect in Gateway balance...`);
let attempts = 0;
const expectedMinBalance = gatewayBalanceNum + Number(depositAmount);
let gatewayUpdated = false;

while (attempts < 30) {
while (attempts < 10) {
balances = await gatewayClient.getBalances();
gatewayBalanceNum = Number(balances.gateway.formattedAvailable);
if (gatewayBalanceNum >= expectedMinBalance) {
console.log(`[Core] ✅ Gateway balance updated! (${gatewayBalanceNum} USDC)`);
console.log(`[Core] Gateway balance updated! (${gatewayBalanceNum} USDC)`);
gatewayUpdated = true;
break;
}
attempts++;
await new Promise(resolve => setTimeout(resolve, 2000));
await new Promise(resolve => setTimeout(resolve, 1500));
}

if (!gatewayUpdated) {
Expand Down Expand Up @@ -487,7 +487,7 @@ coreRouter.post('/tip', sessionLimiter, async (req: Request, res: Response) => {
return res.json({ status: 'success', amount, creatorWallet });
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
if (err.message.includes('402') || err.message.toLowerCase().includes('insufficient')) {
if (err.message.includes('402') || err.message.toLowerCase().includes('insufficient') || err.message.toLowerCase().includes('settlement failed')) {
return res.status(402).json({ error: 'Insufficient gateway balance. Please top up.' });
}
console.error(`[Core] ❌ Tip failed:`, err.message);
Expand Down
6 changes: 6 additions & 0 deletions src/ui/paywall.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
pointer-events: none;
}

#arc-paywall-overlay.arc-tip-mode-overlay {
background: rgba(0, 0, 0, 0.4) !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}

body.arc-locked > *:not(#arc-paywall-overlay):not(iframe) {
filter: blur(16px) grayscale(30%);
pointer-events: none;
Expand Down
94 changes: 78 additions & 16 deletions src/ui/paywall.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,13 +234,13 @@ function injectDependencies() {

function lockMedia() {
document.addEventListener('play', (e) => {
if (document.body.classList.contains('arc-locked') &&
if (!isTipMode && document.body.classList.contains('arc-locked') &&
(e.target.tagName === 'VIDEO' || e.target.tagName === 'AUDIO')) {
e.target.pause();
}
}, true);
setInterval(() => {
if (document.body.classList.contains('arc-locked')) {
if (!isTipMode && document.body.classList.contains('arc-locked')) {
document.querySelectorAll('video, audio').forEach(m => { if (!m.paused) m.pause(); });
}
}, 500);
Expand Down Expand Up @@ -288,7 +288,8 @@ function renderPaywallOverlay(hideInitially = false) {
overlay.classList.add('arc-hidden-initially');
}
overlay.innerHTML = `
<div id="arc-paywall-modal">
<div id="arc-paywall-modal" style="position:relative;">
${isTipMode ? '<button id="arc-paywall-close-btn" class="arc-modal-close" style="position:absolute;top:16px;right:16px;background:none;border:none;color:#a0aec0;font-size:18px;cursor:pointer;z-index:10;">✕</button>' : ''}
<div id="arc-paywall-header">
<div id="arc-paywall-logo">
<img src="${SCRIPT_BASE_DIR}logo_yellow.svg" alt="Tessera" />
Expand Down Expand Up @@ -455,6 +456,27 @@ function renderPaywallOverlay(hideInitially = false) {
document.body.appendChild(overlay);

// Wire up events
if (isTipMode) {
overlay.classList.add('arc-tip-mode-overlay');
document.body.classList.remove('arc-locked');

// Dismiss modal on clicking outside the modal box
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
overlay.remove();
document.body.classList.remove('arc-locked');
}
});

const tipCloseBtn = document.getElementById('arc-paywall-close-btn');
if (tipCloseBtn) {
tipCloseBtn.addEventListener('click', () => {
const ov = document.getElementById('arc-paywall-overlay');
if (ov) ov.remove();
document.body.classList.remove('arc-locked');
});
}
}
document.getElementById('arc-login-btn').addEventListener('click', handleEmailLogin);
document.getElementById('arc-bridge-btn').addEventListener('click', openCctpModal);
document.getElementById('arc-cctp-close').addEventListener('click', closeCctpModal);
Expand Down Expand Up @@ -841,6 +863,8 @@ async function handleUnlock() {
await new Promise(r => setTimeout(r, 2000));
}
if (!confirmed) throw new Error('Deposit timed out. Please try again.');
// Allow 2 seconds for Arc blockchain indexers to register the USDC transfer
await new Promise(r => setTimeout(r, 2000));
}

// Register session with ephemeral key
Expand Down Expand Up @@ -1521,11 +1545,7 @@ window.arcLeaveSession = async function() {
} catch (_) { /* best effort */ }

if (isTipMode) {
// Tipping mode: clear ephemeral session keys and reset the tipping widget UI
localStorage.removeItem('arc_ephemeral_pk');
viewerState.ephemeralPk = null;

// Reset tipping widget to onboarding/connect state
// Tipping mode: reset tipping widget UI while keeping viewer session keys intact for next tips
const container = document.getElementById('arc-tip-btn-container');
if (container) {
container.remove();
Expand All @@ -1534,21 +1554,62 @@ window.arcLeaveSession = async function() {
}
}
} else {
// Pay-per-second mode: lock video and show paused session message
// Pay-per-second mode: lock video and show paused session card with explicit Resume button
const sm = document.getElementById('arc-session-manager');
if (sm) {
sm.innerHTML = `
<div style="padding:10px;">
<div style="padding:10px;text-align:center;">
<h3 style="color:#63b3ed;margin:0 0 8px 0;">⏸ Session Paused</h3>
<p style="font-size:12px;color:#a0aec0;margin:0 0 10px 0;">Your balance is safe. Sign in again with the same email to resume.</p>
<p style="font-size:11px;color:#718096;margin:0;">Billing has stopped.</p>
<p style="font-size:12px;color:#a0aec0;margin:0 0 10px 0;">Your balance is safe in Circle Gateway.</p>
<button id="arc-resume-btn" onclick="window.arcResumeSession()" class="arc-btn arc-btn-primary" style="padding:6px 14px;font-size:12px;cursor:pointer;">▶ Resume Stream</button>
</div>
`;
}
document.body.classList.add('arc-locked');
}
};

window.arcResumeSession = async function() {
const resumeBtn = document.getElementById('arc-resume-btn');
if (resumeBtn) {
resumeBtn.disabled = true;
resumeBtn.innerText = 'Resuming…';
}

try {
viewerState.ephemeralPk = localStorage.getItem('arc_ephemeral_pk');
if (!viewerState.ephemeralPk) {
viewerState.ephemeralPk = '0x' + Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map(b => b.toString(16).padStart(2, '0')).join('');
localStorage.setItem('arc_ephemeral_pk', viewerState.ephemeralPk);
}

const regRes = await fetch(ARC_API_BASE + '/api/core/register-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: viewerState.userId,
privateKey: viewerState.ephemeralPk,
returnAddress: viewerState.walletAddress,
ratePerSecond: getRequiredMinBalance(),
}),
});

if (regRes.ok) {
console.log('[Tessera] Session resumed successfully.');
document.body.classList.remove('arc-locked');
renderSessionManager();
startSessionTimer();
return;
}
} catch (err) {
console.error('[Tessera] Resume session error:', err);
}

// Fallback if silent resume fails: check auto unlock or open onboarding
await checkAutoUnlock();
};

window.arcEndSession = async function() {
const endBtn = document.getElementById('arc-sm-end-btn');
if (endBtn) {
Expand Down Expand Up @@ -1665,7 +1726,9 @@ async function fetchTipBalance() {
function openTipOnboarding() {
if (isTipMode) {
injectDependencies();
document.body.classList.remove('arc-locked');
renderPaywallOverlay();
document.body.classList.remove('arc-locked');
} else if (window.ArcCashier && typeof window.ArcCashier.initPaywall === 'function') {
window.ArcCashier.initPaywall();
} else {
Expand All @@ -1674,6 +1737,9 @@ function openTipOnboarding() {
}

window.arcShowTipButton = function(creatorWallet, tipAmount) {
if (creatorWallet) tipCreatorWallet = creatorWallet;
if (tipAmount) tipAmountVal = tipAmount;

// Remove any existing tip button
const existing = document.getElementById('arc-tip-btn-container');
if (existing) existing.remove();
Expand Down Expand Up @@ -1725,7 +1791,6 @@ window.arcShowTipButton = function(creatorWallet, tipAmount) {
</button>

<div id="arc-tip-wallet-actions" style="display:none;">
<button id="arc-tip-leave-btn">Just Leave</button>
<button id="arc-tip-end-btn">Cash Out &amp; Exit</button>
</div>
</div>
Expand Down Expand Up @@ -1772,7 +1837,6 @@ window.arcShowTipButton = function(creatorWallet, tipAmount) {
const sentVal = document.getElementById('arc-tip-sent-val');
const walletActions = document.getElementById('arc-tip-wallet-actions');

const leaveBtn = document.getElementById('arc-tip-leave-btn');
const endBtn = document.getElementById('arc-tip-end-btn');

btn.addEventListener('mouseenter', () => { btn.style.transform = 'scale(1.03)'; });
Expand Down Expand Up @@ -1814,8 +1878,6 @@ window.arcShowTipButton = function(creatorWallet, tipAmount) {
}
}, 5000);

leaveBtn.addEventListener('click', window.arcLeaveSession);

endBtn.addEventListener('click', async () => {
if (!confirm('Are you sure you want to cash out and exit? This will return your remaining balance to your wallet.')) {
return;
Expand Down
Loading