git / moonbite-network / templates/wallet-pwa-app.html · raw
<!-- Cache Buster: 2026-08-14T22:00:00Z --><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no"> <meta name="theme-color" content="#0B0D12"> <meta name="description" content="MoonBite Wallet - Secure cryptocurrency wallet for MBITE tokens"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-title" content="MoonBite"> <meta name="application-name" content="MoonBite Wallet"> <link rel="manifest" href="/wallet-manifest.json"> <!-- d21/d03: the canonical mark, solid Void tile — the old inline data-URI icon (blue #1a4d7a background, Bitcoin-orange #ff6b35, banned gold #ffd700, a teardrop, not the mark) predated the brand system entirely. --> <link rel="apple-touch-icon" href="/static/moonbite-logo.svg"> <link rel="icon" type="image/svg+xml" href="/static/moonbite-logo.svg"> <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;"> <title>MoonBite Wallet</title> <link rel="stylesheet" href="/static/fonts/moonbite-fonts.css"><style>/* Doc 04: three roles. A heavy grotesque for display, a quiet sans for reading, a monospace for anything that is a number. Archivo Black and Inter are self-hosted from /static/fonts (latin subset, OFL-licensed). They are deliberately not loaded from Google Fonts: /privacy states the site loads nothing that phones home, and a webfont CDN sends every visitor's IP to Google on every page load. Every figure that is money, a hash, an address or a block height is monospace with tabular figures — amounts that shift width as they update look untrustworthy. */:root{ --font-display:'Archivo Black','Helvetica Neue','Arial Black',sans-serif; --font-body:Inter,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; --font-data:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}h1,h2,.big,.reveal .big,.balance-amount,.elapsed,.mort-big{ font-family:var(--font-display); font-weight:400; letter-spacing:-.02em;}.num,.mono,.addr,.cert .amt,.cl .v,.tot .v,.mcell .v,.odds .v,.loss .v,.count .v,.zero-band .v,.total .v,.vault-v,td.n,th.n{ font-family:var(--font-data); font-variant-numeric:tabular-nums;} * { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; } :root { /* MoonBite Reserve Palette. The wallet is variable-driven, so rebranding it is a token swap rather than a rewrite — the right trade for a surface that holds private keys. Legacy names (--gold, --fire-orange, --secondary) are kept and remapped so every existing rule inherits the new brand untouched. */ --primary: #D9A441; --primary-dark: #B8863B; --secondary: #B8863B; --success: #2E9E6B; --danger: #C8402F; --warning: #D9A441; --fire-orange: #B8863B; --gold: #D9A441; --emerald: #2E9E6B; --slate: #161A22; /* Dark (the only mode the brand ships) */ --bg-primary: #0B0D12; --bg-secondary: #161A22; --bg-tertiary: #232833; --text-primary: #F5F2EA; --text-secondary: #8A8580; --text-tertiary: #6E6A66; --border-color: rgba(217, 164, 65, 0.14); --card-bg: rgba(22, 26, 34, 0.6); /* Light Mode */ --bg-primary-light: #F5F2EA; --bg-secondary-light: #EFEBE1; --bg-tertiary-light: #E5E0D4; --text-primary-light: #0B0D12; --text-secondary-light: #6E6A66; --text-tertiary-light: #8A8580; --border-color-light: #D8D2C4; --card-bg-light: #EFEBE1; --spacing-xs: 4px; --spacing-sm: 8px; --spacing-md: 16px; --spacing-lg: 24px; --spacing-xl: 32px; } /* Light Theme */ html[data-theme="light"] { --bg-primary: var(--bg-primary-light); --bg-secondary: var(--bg-secondary-light); --bg-tertiary: var(--bg-tertiary-light); --text-primary: var(--text-primary-light); --text-secondary: var(--text-secondary-light); --text-tertiary: var(--text-tertiary-light); --border-color: var(--border-color-light); --card-bg: var(--card-bg-light); } html, body { width: 100%; height: 100%; font-family: var(--font-body); background: var(--bg-primary); color: var(--text-primary); overflow: hidden; position: fixed; transition: background 0.3s ease, color 0.3s ease; } body { display: flex; flex-direction: column; } /* Theme Toggle Button */ .theme-toggle { position: fixed; bottom: 100px; right: 20px; width: 50px; height: 50px; border-radius: 50%; background: linear-gradient(135deg, var(--primary), var(--primary-dark)); border: none; color: white; font-size: 20px; cursor: pointer; box-shadow: 0 4px 16px rgba(0, 102, 255, 0.3); z-index: 90; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; } .theme-toggle:hover { transform: scale(1.1); box-shadow: 0 8px 24px rgba(0, 102, 255, 0.4); } .app-container { flex: 1; display: flex; flex-direction: column; width: 100%; height: 100%; max-width: 428px; margin: 0 auto; background: var(--dark-bg); position: relative; overflow: hidden; } /* Premium Top bar */ .top-bar { display: flex; align-items: center; justify-content: space-between; padding: 10px var(--spacing-md); background: linear-gradient(180deg, rgba(26, 77, 122, 0.3) 0%, rgba(26, 77, 122, 0.1) 100%); border-bottom: 1px solid rgba(184, 134, 59, 0.2); backdrop-filter: blur(20px); z-index: 100; } .top-bar.premium-header { padding: 8px var(--spacing-md); background: linear-gradient(135deg, rgba(26, 77, 122, 0.4) 0%, rgba(184, 134, 59, 0.1) 100%); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); } .logo-brand { display: flex; align-items: center; gap: 12px; } .header-logo { height: 40px; width: auto; object-fit: contain; filter: drop-shadow(0 4px 12px rgba(184, 134, 59, 0.4)); transition: transform 0.3s ease; } .header-logo:hover { transform: scale(1.05); } .top-bar-left { display: flex; align-items: center; gap: var(--spacing-sm); } .back-btn { background: none; border: none; color: var(--gold); font-size: 24px; cursor: pointer; padding: var(--spacing-xs); /* A 24px glyph in 4px padding was ~32px of tappable area; give it a real 44px target. */ min-width: 44px; min-height: 44px; display: flex; align-items: center; justify-content: center; } .back-btn.hidden { visibility: hidden; } .top-bar-title { font-size: 18px; font-weight: 600; color: var(--gold); } .top-bar-right { display: flex; gap: var(--spacing-sm); } .icon-btn { background: none; border: none; color: var(--fire-orange); font-size: 20px; cursor: pointer; padding: var(--spacing-xs); display: flex; align-items: center; justify-content: center; } /* Main content area */ .screens-container { flex: 1; position: relative; overflow: hidden; } .screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none !important; flex-direction: column; overflow-y: auto; overflow-x: hidden; opacity: 0 !important; transform: translateX(100%) !important; transition: opacity 0.3s ease, transform 0.3s ease; padding-bottom: 80px; visibility: hidden !important; pointer-events: none !important; z-index: 1; } .screen.active { display: flex !important; opacity: 1 !important; transform: translateX(0) !important; visibility: visible !important; pointer-events: auto !important; z-index: 10; } .screen.prev { transform: translateX(-100%) !important; } /* Bottom navigation */ .bottom-nav { position: fixed; bottom: 0; left: 0; right: 0; max-width: 428px; margin: 0 auto; background: rgba(26, 77, 122, 0.15); border-top: 1px solid var(--border-color); backdrop-filter: blur(10px); /* Hidden by default so it never shows on the seed/PIN onboarding screens, where it let a visitor tap straight into the wallet before setting anything up. showScreen() reveals it only on the signed-in app screens. */ display: none; justify-content: space-around; padding: var(--spacing-sm) 0; z-index: 100; } .bottom-nav.show { display: flex; } .nav-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 8px var(--spacing-sm); background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 12px; transition: color 0.3s ease; text-decoration: none; } .nav-item.active { color: var(--gold); } .nav-icon { font-size: 24px; } /* Screens styling - Luxury Edition */ .screen-header { padding: var(--spacing-lg) var(--spacing-md); text-align: center; background: linear-gradient(180deg, rgba(26, 31, 58, 0.4) 0%, rgba(21, 26, 47, 0.2) 100%); border-bottom: 1px solid rgba(217, 164, 65, 0.08); } .screen-header h1 { font-size: 26px; color: var(--gold); margin-bottom: 6px; font-weight: 600; letter-spacing: 0.3px; } .screen-header p { color: var(--text-secondary); font-size: 13px; letter-spacing: 0.2px; font-weight: 400; } .screen-content { flex: 1; padding: var(--spacing-lg) var(--spacing-md); overflow-y: auto; } /* Cards */ .card { background: linear-gradient(135deg, rgba(26, 77, 122, 0.15), rgba(184, 134, 59, 0.05)); border: 1px solid var(--border-color); border-radius: 12px; padding: var(--spacing-md); margin-bottom: var(--spacing-md); } .card-title { font-size: 14px; color: var(--text-secondary); margin-bottom: var(--spacing-sm); font-weight: 500; } .card-value { font-size: 24px; color: var(--gold); font-weight: 600; margin-bottom: var(--spacing-sm); } .card-subtitle { font-size: 12px; color: var(--text-tertiary); } /* Luxury Premium Balance Card - God-Level Edition */ .premium-balance-card { position: relative; border-radius: 24px; padding: 22px; margin-bottom: 18px; min-height: 200px; display: flex; flex-direction: column; justify-content: space-between; overflow: hidden; /* Slate is the surface for panels; Moongold is an accent, not a background. A gold slab reads as a memecoin — the brand's own rule is restraint. */ background: linear-gradient(168deg, #1F242F 0%, #161A22 55%, #151921 100%); border: 1px solid rgba(217,164,65,.20); box-shadow: 0 24px 60px rgba(0,0,0,.55); } /* The only gold on the card besides the mark: one hairline along the top. */ .premium-balance-card::before { content: ''; position: absolute; top: 0; left: 22px; right: 22px; height: 1px; background: linear-gradient(90deg, rgba(217,164,65,0), #D9A441 42%, rgba(217,164,65,0)); } .vault-top { display: flex; align-items: center; justify-content: space-between; } .vault-brand { display: flex; align-items: center; gap: 9px; color: #F5F2EA; font-weight: 800; font-size: .68rem; letter-spacing: .3em; } .vault-net { font-size: .54rem; letter-spacing: .2em; font-weight: 800; color: #8A8580; border: 1px solid rgba(138,133,128,.35); padding: 4px 9px; border-radius: 999px; } .vault-mid { margin: 4px 0; } .balance-label { font-size: .56rem; letter-spacing: .24em; text-transform: uppercase; font-weight: 800; color: #8A8580; } .balance-amount { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: clamp(1.9rem, 9vw, 2.7rem); font-weight: 800; line-height: 1; letter-spacing: -.035em; margin-top: 10px; color: #F5F2EA; font-variant-numeric: tabular-nums; display: flex; align-items: baseline; gap: 10px; } /* Doc 02: the glyph sets on the baseline at cap height and takes the colour of surrounding text, except the struck bars which stay Moongold. */ .mbite-glyph { height: .58em; width: auto; color: #F5F2EA; flex: none; position: relative; top: .02em; } .vault-sub { font-size: .74rem; font-weight: 700; color: #8A8580; margin-top: 9px; letter-spacing: .04em; } .vault-bottom { display: flex; justify-content: space-between; align-items: flex-end; border-top: 1px solid rgba(245,242,234,.07); padding-top: 14px; } .vault-k { font-size: .5rem; letter-spacing: .2em; text-transform: uppercase; font-weight: 800; color: #8A8580; } .vault-v { font-size: .84rem; font-weight: 800; letter-spacing: .08em; margin-top: 4px; color: #F5F2EA; } .dash-content { padding-top: 16px; } @keyframes cardViral { 0%, 100% { box-shadow: 0 30px 100px rgba(0, 51, 166, 0.4), 0 0 80px rgba(184, 134, 59, 0.3), inset 0 1px 20px rgba(255, 255, 255, 0.15); } 50% { box-shadow: 0 40px 120px rgba(0, 51, 166, 0.5), 0 0 100px rgba(184, 134, 59, 0.4), inset 0 1px 20px rgba(255, 255, 255, 0.2); } } .premium-balance-card::before { content: ''; position: absolute; top: -50%; right: -20%; width: 400px; height: 400px; background: radial-gradient(circle, rgba(255, 255, 255, 0.2) 0%, rgba(217, 164, 65, 0.1) 50%, transparent 70%); border-radius: 50%; pointer-events: none; animation: floatViral1 6s ease-in-out infinite; } @keyframes floatViral1 { 0%, 100% { transform: translate(0, 0) scale(1); } 50% { transform: translate(20px, -20px) scale(1.1); } } .premium-balance-card::after { content: ''; position: absolute; bottom: -100px; left: -100px; width: 400px; height: 400px; background: radial-gradient(circle, rgba(184, 134, 59, 0.15) 0%, transparent 70%); border-radius: 50%; pointer-events: none; animation: floatViral2 8s ease-in-out infinite; } @keyframes floatViral2 { 0%, 100% { transform: translate(0, 0) scale(1); } 50% { transform: translate(-20px, 20px) scale(1.1); } } html[data-theme="light"] .premium-balance-card { background: linear-gradient(168deg, #FFFFFF 0%, #EFEBE1 100%); border-color: rgba(217,164,65,.35); } html[data-theme="light"] .balance-amount, html[data-theme="light"] .vault-v, html[data-theme="light"] .vault-brand { color: #0B0D12; } /* superseded by the vault card rule above */ @keyframes balancePulse { 0%, 100% { transform: scale(1) translateY(0); } 50% { transform: scale(1.03) translateY(-2px); } } .balance-unit { font-size: 22px; font-weight: 700; margin-left: 8px; } html[data-theme="light"] .balance-label, html[data-theme="light"] .balance-amount, .card-logo-img { height: 24px; width: auto; filter: drop-shadow(0 2px 6px rgba(255, 214, 0, 0.3)); } .premium-wallet-card { background: linear-gradient(135deg, rgba(255, 214, 0, 0.1) 0%, rgba(184, 134, 59, 0.08) 100%); border: 1px solid rgba(255, 214, 0, 0.3); border-radius: 16px; padding: var(--spacing-lg); margin-bottom: var(--spacing-lg); display: flex; align-items: center; gap: var(--spacing-lg); box-shadow: 0 8px 24px rgba(255, 214, 0, 0.1); backdrop-filter: blur(10px); } .wallet-icon { font-size: 32px; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, rgba(255, 214, 0, 0.3), rgba(184, 134, 59, 0.2)); width: 56px; height: 56px; border-radius: 12px; flex-shrink: 0; border: 1px solid rgba(255, 214, 0, 0.2); } .wallet-details { flex: 1; } .wallet-label { font-size: 11px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; font-weight: 600; } .wallet-name-display { font-size: 18px; font-weight: 600; color: var(--text-primary); margin-bottom: 6px; } .wallet-amount { font-size: 15px; color: var(--gold); font-weight: 500; } .action-buttons { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: var(--spacing-lg); margin-top: var(--spacing-lg); } .action-btn { flex: 1; background: rgba(22,26,34,.9); border: 1px solid rgba(217,164,65,.22); border-radius: 16px; padding: 15px 8px; color: #F5F2EA; font-weight: 700; font-size: .68rem; letter-spacing: .12em; text-transform: uppercase; cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 7px; min-height: 68px; transition: border-color .2s ease, transform .15s ease; } .action-btn::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: radial-gradient(circle at center, rgba(255, 214, 0, 0.15), transparent); transition: left 0.4s ease; pointer-events: none; } .action-btn:hover { background: linear-gradient(135deg, rgba(255, 214, 0, 0.18) 0%, rgba(184, 134, 59, 0.12) 100%); transform: translateY(-3px); box-shadow: 0 12px 28px rgba(255, 214, 0, 0.15), 0 0 20px rgba(184, 134, 59, 0.1); border-color: rgba(255, 214, 0, 0.35); } .action-btn:hover::before { left: 100%; } .action-btn svg { width: 24px; height: 24px; color: var(--gold); } .premium-icon { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; } /* Forms */ .form-group { margin-bottom: var(--spacing-md); } .form-label { display: block; margin-bottom: var(--spacing-sm); color: var(--text-primary); font-size: 14px; font-weight: 500; } .form-input { width: 100%; padding: 12px var(--spacing-md); background: rgba(26, 77, 122, 0.1); border: 1px solid var(--border-color); border-radius: 8px; color: var(--text-primary); font-size: 14px; transition: border-color 0.3s ease; } .form-input:focus { outline: none; border-color: var(--fire-orange); background: rgba(26, 77, 122, 0.15); } /* Buttons */ .btn { padding: 12px var(--spacing-md); /* 48px is the tap-target floor for reliable finger use. Padding alone left the shorter secondary buttons around 40px. */ min-height: 48px; display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; text-transform: uppercase; letter-spacing: 0.5px; } .btn-primary { /* The one gold fill per screen — this is what 'sparingly' buys you. */ background: #D9A441; color: #0B0D12; width: 100%; margin-top: var(--spacing-md); box-shadow: 0 12px 30px rgba(184, 134, 59, 0.25); transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 16px 40px rgba(184, 134, 59, 0.35); } .btn-primary:active { transform: scale(0.98); opacity: 0.95; } .btn-secondary { background: rgba(26, 31, 58, 0.4); color: var(--gold); border: 1px solid rgba(217, 164, 65, 0.25); width: 100%; margin-top: var(--spacing-sm); } .btn-small { padding: 8px var(--spacing-md); font-size: 12px; width: auto; } /* Wallet address display */ .address-display { background: rgba(0, 0, 0, 0.3); border: 1px solid var(--gold); border-radius: 8px; padding: var(--spacing-md); font-family: 'Monaco', 'Menlo', monospace; font-size: 12px; color: var(--gold); word-break: break-all; margin: var(--spacing-md) 0; } /* QR Code */ .qr-placeholder { width: 200px; height: 200px; background: linear-gradient(135deg, rgba(184, 134, 59, 0.2), rgba(217, 164, 65, 0.2)); border: 2px solid var(--fire-orange); border-radius: 12px; margin: var(--spacing-lg) auto; display: flex; align-items: center; justify-content: center; color: var(--text-secondary); font-size: 48px; } /* Toggles */ .toggle { display: flex; align-items: center; justify-content: space-between; padding: var(--spacing-md) 0; border-bottom: 1px solid var(--border-color); } .toggle-switch { width: 50px; height: 28px; background: rgba(26, 77, 122, 0.3); border: 2px solid var(--border-color); border-radius: 14px; cursor: pointer; position: relative; transition: all 0.3s ease; } .toggle-switch.active { background: linear-gradient(135deg, var(--fire-orange), #B8863B); border-color: var(--fire-orange); } .toggle-switch::after { content: ''; position: absolute; width: 22px; height: 22px; background: white; border-radius: 12px; top: 2px; left: 2px; transition: left 0.3s ease; } .toggle-switch.active::after { left: 22px; } /* Seed phrase grid */ .seed-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-md); margin: var(--spacing-md) 0; } .seed-item { background: rgba(26, 77, 122, 0.1); border: 1px solid var(--fire-orange); border-radius: 8px; padding: var(--spacing-md); text-align: center; } .seed-number { font-size: 12px; color: var(--text-secondary); margin-bottom: var(--spacing-xs); } .seed-word { font-size: 16px; color: var(--gold); font-weight: 600; font-family: 'Monaco', 'Menlo', monospace; } /* Install prompt */ .install-prompt { background: linear-gradient(135deg, var(--fire-orange), #B8863B); padding: var(--spacing-md); border-radius: 12px; margin-bottom: var(--spacing-md); color: white; text-align: center; display: none; } .install-prompt.show { display: block; } .install-prompt-title { font-weight: 600; margin-bottom: var(--spacing-sm); font-size: 14px; } .install-prompt-text { font-size: 12px; margin-bottom: var(--spacing-md); opacity: 0.9; } .install-prompt-buttons { display: flex; gap: var(--spacing-sm); justify-content: center; } .install-prompt-btn { padding: 8px var(--spacing-md); border: none; border-radius: 6px; font-size: 12px; cursor: pointer; font-weight: 600; background: rgba(255, 255, 255, 0.2); color: white; backdrop-filter: blur(10px); } .install-prompt-btn:active { background: rgba(255, 255, 255, 0.3); } /* Wallet list - Luxury Style */ .wallet-item { background: linear-gradient(135deg, rgba(26, 31, 58, 0.5) 0%, rgba(21, 26, 47, 0.3) 100%); border: 1px solid rgba(217, 164, 65, 0.12); border-radius: 14px; padding: 16px; margin-bottom: var(--spacing-md); cursor: pointer; transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } .wallet-item:hover { background: linear-gradient(135deg, rgba(26, 31, 58, 0.7) 0%, rgba(21, 26, 47, 0.5) 100%); border-color: rgba(217, 164, 65, 0.25); transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); } .wallet-item:active { transform: scale(0.98); border-color: var(--fire-orange); } .wallet-name { font-size: 16px; color: var(--text-primary); margin-bottom: var(--spacing-xs); } .wallet-balance { font-size: 20px; color: var(--gold); font-weight: 600; margin-bottom: var(--spacing-xs); } /* Contact list */ .contact-item { display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-md) 0; border-bottom: 1px solid var(--border-color); cursor: pointer; } .contact-avatar { width: 48px; height: 48px; border-radius: 50%; background: linear-gradient(135deg, var(--fire-orange), var(--gold)); display: flex; align-items: center; justify-content: center; color: var(--dark-bg); font-weight: 600; flex-shrink: 0; } .contact-info { flex: 1; min-width: 0; } .contact-name { font-size: 16px; color: var(--text-primary); margin-bottom: var(--spacing-xs); } .contact-address { font-size: 12px; color: var(--text-secondary); font-family: 'Monaco', 'Menlo', monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Success state */ .success-icon { width: 80px; height: 80px; background: conic-gradient(from 0deg, var(--fire-orange), var(--gold)); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: var(--spacing-xl) auto; font-size: 40px; } /* Scroll behavior */ .screen-content::-webkit-scrollbar { width: 6px; } .screen-content::-webkit-scrollbar-track { background: transparent; } .screen-content::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 3px; } /* Responsive */ @media (max-width: 480px) { .app-container { max-width: 100%; } } @media (max-height: 600px) { .top-bar { padding: 8px var(--spacing-md); } .screen-header { padding: var(--spacing-md); } .screen-header h1 { font-size: 20px; } .screen-content { padding: var(--spacing-md); } } /* Onboarding Animations */ @keyframes pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.2); opacity: 0.7; } } @keyframes scanPulse { 0%, 100% { transform: scale(1); opacity: 0.3; } 50% { transform: scale(1.1); opacity: 0.7; } } @keyframes pinPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } } /* EMPTY STATE: User has 0 MBITE */ .empty-state-card { padding: var(--spacing-lg); background: linear-gradient(135deg, rgba(22,26,34,0.4) 0%, rgba(35,40,51,0.4) 100%); border: 1px solid var(--border-color); border-radius: 10px; margin-bottom: var(--spacing-lg); } .empty-hero { text-align: center; margin-bottom: var(--spacing-xl); } .empty-icon { font-size: 3rem; margin-bottom: var(--spacing-md); display: block; } .empty-hero h2 { color: var(--text-primary); margin-bottom: var(--spacing-sm); font-size: 1.5rem; } .empty-subtitle { color: var(--text-secondary); font-size: 0.95rem; line-height: 1.5; } .empty-paths { display: grid; grid-template-columns: 1fr; gap: var(--spacing-md); margin-bottom: var(--spacing-xl); } .path-card { background: rgba(22,26,34,0.5); border: 1px solid var(--border-color); border-radius: 10px; padding: var(--spacing-md); } .path-title { font-family: var(--font-display); color: var(--primary); font-size: 1rem; font-weight: 400; letter-spacing: -0.02em; margin-bottom: var(--spacing-sm); } .path-desc { color: var(--text-secondary); font-size: 0.9rem; line-height: 1.5; margin-bottom: var(--spacing-md); } .path-link { display: inline-block; color: var(--primary); text-decoration: none; font-weight: 600; font-size: 0.9rem; background: none; border: none; cursor: pointer; padding: 0; transition: color 0.2s; } .path-link:hover { color: var(--primary-dark); } .path-link:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: 2px; } .empty-address-box { background: rgba(217,164,65,0.08); border: 1px solid var(--border-color); border-radius: 10px; padding: var(--spacing-md); text-align: center; } .addr-label { color: var(--text-secondary); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: var(--spacing-sm); display: block; } .addr-display { background: rgba(0,0,0,0.3); color: var(--primary); padding: var(--spacing-md); border-radius: 6px; font-size: 0.9rem; word-break: break-all; margin-bottom: var(--spacing-md); user-select: all; } .addr-copy-btn { background: var(--primary); color: var(--bg-primary); border: none; border-radius: 6px; padding: var(--spacing-sm) var(--spacing-md); min-height: 44px; font-weight: 600; font-size: 0.9rem; cursor: pointer; transition: background 0.2s; } .addr-copy-btn:hover { background: var(--primary-dark); } .addr-copy-btn:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } /* BALANCE STATE: User has MBITE */ .balance-state-card { animation: fadeIn 0.3s ease-in; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } /* Premium SVG icons */ .premium-icon { width: 60px; height: 60px; margin: 0 auto var(--spacing-lg); display: block; opacity: 0.9; } .empty-icon-svg { width: 80px; height: 80px; margin: 0 auto var(--spacing-lg); display: block; opacity: 0.85; } .wallet-icon svg { width: 28px; height: 28px; display: block; } /* Premium depth & shadows */ .screen { box-shadow: 0 -2px 16px rgba(0, 0, 0, 0.4); background: linear-gradient(180deg, rgba(11, 13, 18, 0.95) 0%, rgba(22, 26, 34, 0.95) 100%); } .premium-balance-card { background: linear-gradient(135deg, rgba(22, 26, 34, 0.8) 0%, rgba(35, 40, 51, 0.8) 100%); box-shadow: inset 0 1px 0 rgba(217, 164, 65, 0.08), 0 8px 24px rgba(0, 0, 0, 0.32); border: 1px solid rgba(217, 164, 65, 0.12); } .premium-wallet-card { background: linear-gradient(135deg, rgba(35, 40, 51, 0.6) 0%, rgba(22, 26, 34, 0.6) 100%); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.24); border: 1px solid rgba(217, 164, 65, 0.08); } .action-buttons { gap: var(--spacing-md); } .action-btn { background: linear-gradient(135deg, rgba(217, 164, 65, 0.12) 0%, rgba(217, 164, 65, 0.04) 100%); border: 1px solid rgba(217, 164, 65, 0.14); color: var(--text-primary); font-weight: 500; letter-spacing: 0.3px; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); } .action-btn:hover { background: rgba(217, 164, 65, 0.16); box-shadow: 0 4px 16px rgba(217, 164, 65, 0.08); transform: translateY(-1px); } .action-btn:active { transform: translateY(0); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.16); } /* Premium typography upgrades */ h1 { letter-spacing: -0.03em; text-transform: none; } .screen-header { border-bottom: 1px solid rgba(217, 164, 65, 0.08); padding-bottom: var(--spacing-md); } /* Mobile responsive */ @media (max-width: 600px) { .empty-state-card { padding: var(--spacing-md); } .empty-hero h2 { font-size: 1.25rem; } .path-card { padding: var(--spacing-md); } .empty-icon { font-size: 2.5rem; } .premium-icon { width: 48px; height: 48px; } .empty-icon-svg { width: 64px; height: 64px; } } </style></head><body> <!-- Splash: covers the launch until the crypto modules have loaded, so the PIN/seed screens are never interactive before the code that unlocks them is ready. Fades out on the 'moonbite-ready' event (or a timeout fallback so it can never trap the user). --> <div id="splashScreen" style="position:fixed; inset:0; z-index:9999; background:#0B0D12; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:22px; transition:opacity .4s ease;"> <img src="/static/moonbite-logo.svg" alt="MoonBite" style="width:88px; height:auto;" loading="eager"> <div style="font-size:15px; letter-spacing:.34em; text-transform:uppercase; color:#D9A441; font-weight:700;">MoonBite</div> <div style="width:32px; height:32px; border:3px solid rgba(217,164,65,0.2); border-top-color:#D9A441; border-radius:50%; animation:mbSpin .8s linear infinite;"></div> <div style="font-size:12px; color:#8A8580;">Securing your wallet…</div> </div> <style>@keyframes mbSpin { to { transform: rotate(360deg); } }</style> <div class="app-container"> <!-- Premium Top Bar with Logo --> <div class="top-bar premium-header"> <div class="top-bar-left"> <button class="back-btn" id="backBtn" title="Go back" aria-label="Go back"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg></button> <div class="logo-brand"> <img src="/static/moonbite-logo.svg" alt="MoonBite" class="header-logo" loading="eager"> </div> </div> <div class="top-bar-right"> <button class="icon-btn premium-icon" id="notificationBtn" title="Notifications"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path> <path d="M13.73 21a2 2 0 0 1-3.46 0"></path> </svg> </button> <button class="icon-btn premium-icon" id="settingsBtn" title="Settings"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="3"></circle> <path d="M12 1v6m0 6v6M4.22 4.22l4.24 4.24m2.12 2.12l4.24 4.24M1 12h6m6 0h6m-16.78 7.78l4.24-4.24m2.12-2.12l4.24-4.24"></path> </svg> </button> </div> </div> <!-- Screens Container - Onboarding Enabled --> <div class="screens-container"> <!-- 6-Digit PIN Setup Screen - ONE TIME ONLY --> <div class="screen" id="pinSetupScreen"> <div class="screen-header"> <h1>Create PIN</h1> <p>Secure your wallet</p> </div> <div class="screen-content" style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--spacing-lg); overflow-y: auto;"> <div style="text-align: center; width: 100%; max-width: 400px;"> <!-- STEP 1: ENTER PIN --> <div id="pinCreateStep" style="display: block;"> <svg class="premium-icon" viewBox="0 0 24 24" fill="none" stroke="var(--primary)" stroke-width="1.5"> <path d="M21 2H3a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1h18a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1z"/> <circle cx="12" cy="15" r="3" stroke="var(--primary)"/> </svg> <p style="color: var(--text-secondary); margin-bottom: var(--spacing-lg); font-size: 14px;">Enter a 6-digit PIN to secure your wallet</p> <label class="form-label" style="margin-bottom: var(--spacing-md); display: block; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Create PIN</label> <div style="display: flex; gap: var(--spacing-md); justify-content: center; margin-bottom: var(--spacing-xl);"> <input type="text" class="form-input" id="pinDigit1" maxlength="1" style="width: 50px; height: 50px; text-align: center; font-size: 24px; font-weight: bold;" placeholder="•" data-act="pinfocus" data-arg="pinDigit2"> <input type="text" class="form-input" id="pinDigit2" maxlength="1" style="width: 50px; height: 50px; text-align: center; font-size: 24px; font-weight: bold;" placeholder="•" data-act="pinfocus" data-arg="pinDigit3"> <input type="text" class="form-input" id="pinDigit3" maxlength="1" style="width: 50px; height: 50px; text-align: center; font-size: 24px; font-weight: bold;" placeholder="•" data-act="pinfocus" data-arg="pinDigit4"> <input type="text" class="form-input" id="pinDigit4" maxlength="1" style="width: 50px; height: 50px; text-align: center; font-size: 24px; font-weight: bold;" placeholder="•" data-act="pinfocus" data-arg="pinDigit5"> <input type="text" class="form-input" id="pinDigit5" maxlength="1" style="width: 50px; height: 50px; text-align: center; font-size: 24px; font-weight: bold;" placeholder="•" data-act="pinfocus" data-arg="pinDigit6"> <input type="text" class="form-input" id="pinDigit6" maxlength="1" style="width: 50px; height: 50px; text-align: center; font-size: 24px; font-weight: bold;" placeholder="•" data-act="call" data-fn="handlePinNext"> </div> <button class="btn btn-primary" data-act="call" data-fn="proceedToConfirmPin" style="width: 100%;">Next → Confirm PIN</button> </div> <!-- STEP 2: CONFIRM PIN --> <div id="pinConfirmStep" style="display: none;"> <div style="margin-bottom: var(--spacing-lg); color: #2E9E6B;"><svg viewBox="0 0 24 24" width="56" height="56" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg></div> <p style="color: var(--text-secondary); margin-bottom: var(--spacing-lg); font-size: 14px;">Confirm your PIN to ensure it's correct</p> <label class="form-label" style="margin-bottom: var(--spacing-md); display: block; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Confirm PIN</label> <div style="display: flex; gap: var(--spacing-md); justify-content: center; margin-bottom: var(--spacing-xl);"> <input type="text" class="form-input" id="pinConfirm1" maxlength="1" style="width: 50px; height: 50px; text-align: center; font-size: 24px; font-weight: bold;" placeholder="•" data-act="pinfocus" data-arg="pinConfirm2"> <input type="text" class="form-input" id="pinConfirm2" maxlength="1" style="width: 50px; height: 50px; text-align: center; font-size: 24px; font-weight: bold;" placeholder="•" data-act="pinfocus" data-arg="pinConfirm3"> <input type="text" class="form-input" id="pinConfirm3" maxlength="1" style="width: 50px; height: 50px; text-align: center; font-size: 24px; font-weight: bold;" placeholder="•" data-act="pinfocus" data-arg="pinConfirm4"> <input type="text" class="form-input" id="pinConfirm4" maxlength="1" style="width: 50px; height: 50px; text-align: center; font-size: 24px; font-weight: bold;" placeholder="•" data-act="pinfocus" data-arg="pinConfirm5"> <input type="text" class="form-input" id="pinConfirm5" maxlength="1" style="width: 50px; height: 50px; text-align: center; font-size: 24px; font-weight: bold;" placeholder="•" data-act="pinfocus" data-arg="pinConfirm6"> <input type="text" class="form-input" id="pinConfirm6" maxlength="1" style="width: 50px; height: 50px; text-align: center; font-size: 24px; font-weight: bold;" placeholder="•" data-act="call" data-fn="handlePinConfirmComplete"> </div> <div id="pinMismatchError" style="color: #C8402F; font-size: 12px; margin-bottom: var(--spacing-md); display: none;">PINs do not match. Please try again.</div> <button class="btn btn-primary" data-act="call" data-fn="saveSetupPINAndContinue" style="width: 100%;">Save PIN & Continue</button> <button class="btn btn-secondary" data-act="call" data-fn="backToCreatePin" style="width: 100%; margin-top: var(--spacing-md);">← Back</button> </div> </div> </div> </div> <!-- PIN Verification Screen (For Returning Users) - BIOMETRIC FIRST --> <div class="screen" id="pinVerifyScreen"> <div class="screen-header"> <h1>Welcome Back</h1> <p>Quick login</p> </div> <div class="screen-content" style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--spacing-xl); padding: var(--spacing-xl) var(--spacing-lg);"> <div style="color: var(--gold, #D9A441);"><svg viewBox="0 0 24 24" width="72" height="72" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><path d="M9 9h.01M15 9h.01"/></svg></div> <!-- PIN entry, shown immediately. A biometric-login button used to sit above this without ever calling WebAuthn - it went straight to the dashboard with no authentication and no decrypted seed. Until a real passkey flow exists, the PIN is the login. --> <div id="pinFallbackArea" style="width: 100%; max-width: 300px;"> <label class="form-label" style="margin-bottom: var(--spacing-md); display: block; color: var(--text-secondary); font-size: 13px;">Enter your PIN</label> <div style="display: flex; gap: 8px; justify-content: center; margin-bottom: var(--spacing-lg);"> <input type="text" maxlength="1" class="pin-input" style="width: 50px; height: 50px; text-align: center; font-size: 24px; border: 2px solid var(--border-color); border-radius: 8px; background: rgba(26, 77, 122, 0.1); color: var(--gold);" id="verifyPin0" data-act="pinfocus" data-arg="verifyPin1"> <input type="text" maxlength="1" class="pin-input" style="width: 50px; height: 50px; text-align: center; font-size: 24px; border: 2px solid var(--border-color); border-radius: 8px; background: rgba(26, 77, 122, 0.1); color: var(--gold);" id="verifyPin1" data-act="pinfocus" data-arg="verifyPin2"> <input type="text" maxlength="1" class="pin-input" style="width: 50px; height: 50px; text-align: center; font-size: 24px; border: 2px solid var(--border-color); border-radius: 8px; background: rgba(26, 77, 122, 0.1); color: var(--gold);" id="verifyPin2" data-act="pinfocus" data-arg="verifyPin3"> <input type="text" maxlength="1" class="pin-input" style="width: 50px; height: 50px; text-align: center; font-size: 24px; border: 2px solid var(--border-color); border-radius: 8px; background: rgba(26, 77, 122, 0.1); color: var(--gold);" id="verifyPin3" data-act="pinfocus" data-arg="verifyPin4"> <input type="text" maxlength="1" class="pin-input" style="width: 50px; height: 50px; text-align: center; font-size: 24px; border: 2px solid var(--border-color); border-radius: 8px; background: rgba(26, 77, 122, 0.1); color: var(--gold);" id="verifyPin4" data-act="pinfocus" data-arg="verifyPin5"> <input type="text" maxlength="1" class="pin-input" style="width: 50px; height: 50px; text-align: center; font-size: 24px; border: 2px solid var(--border-color); border-radius: 8px; background: rgba(26, 77, 122, 0.1); color: var(--gold);" id="verifyPin5" data-act="call" data-fn="handlePinFallbackComplete"> </div> <div id="pinVerifyError" style="color: #C8402F; font-size: 13px; display: none; text-align: center; margin-bottom: var(--spacing-md);">Incorrect PIN. Please try again.</div> <button class="btn btn-primary" data-act="call" data-fn="verifyPINAndLogin" style="width: 100%;">Unlock Wallet →</button> <button class="btn btn-secondary" data-act="screen" data-arg="pinRecoveryScreen" style="width: 100%; margin-top: var(--spacing-md);">Forgot PIN? Use Seed Phrase</button> </div> </div> </div> <!-- Wallet Setup Screen (the new-user fork: one wallet, two ways in) --> <div class="screen" id="walletSetupScreen"> <div class="screen-header"> <h1>Set Up Your Wallet</h1> <p>You have one MoonBite wallet. Create a new recovery phrase, or bring the one you already have.</p> </div> <div class="screen-content" style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--spacing-lg); text-align: center;"> <div style="width: 96px; height: 96px; display: flex; align-items: center; justify-content: center;"> <img src="/static/moonbite-logo.svg" alt="MoonBite" style="width: 96px; height: auto;"> </div> <div> <h3 style="color: var(--gold); margin-bottom: var(--spacing-sm);">Welcome to MoonBite</h3> <p style="color: var(--text-secondary);">Your recovery phrase is your wallet. It is created on this device and never sent anywhere.</p> </div> <button class="btn btn-primary" data-act="call" data-fn="createNewWallet" style="width: 100%; max-width: 320px;">Create a New Wallet</button> <button class="btn btn-secondary" data-act="screen" data-arg="seedPhraseScreen" style="width: 100%; max-width: 320px;">I Already Have a Recovery Phrase</button> </div> </div> <!-- Generated Recovery Phrase — shown after "Create a New Wallet" --> <div class="screen" id="createWalletScreen"> <div class="screen-header"> <h1>Your Recovery Phrase</h1> <p>Write these 12 words down, in order, and keep them somewhere safe.</p> </div> <div class="screen-content"> <div class="card" style="background: rgba(192,57,67,0.08); border-color: var(--danger, #C0392B); margin-bottom: var(--spacing-md);"> <div style="font-size:.6rem; letter-spacing:.16em; text-transform:uppercase; color:var(--danger,#C0392B); font-weight:700; margin-bottom:6px;">Critical</div> <div style="color: var(--text-secondary); font-size: 13px; line-height:1.5;"> This phrase is the only way to recover your one wallet. Anyone who has it can take your funds. MoonBite cannot restore it for you — never share it, and never store it online. </div> </div> <div id="generatedPhraseDisplay" style="font-family: var(--font-data, ui-monospace, monospace); font-size: 16px; line-height: 1.9; color: var(--gold); background: rgba(217,164,65,0.06); border: 1px solid rgba(217,164,65,0.18); border-radius: 12px; padding: var(--spacing-md); word-spacing: 6px; text-align: center;"></div> <button class="btn btn-secondary" data-act="call" data-fn="copyGeneratedPhrase" style="width: 100%; margin-top: var(--spacing-md);">Copy Phrase</button> <label style="display:flex; align-items:center; gap:10px; margin-top: var(--spacing-lg); font-size: 13px; color: var(--text-primary); cursor:pointer;"> <input type="checkbox" id="phraseBackedUp" data-act="gate" data-arg="phraseContinueBtn" style="width:18px; height:18px; flex:none;"> I have written down my recovery phrase </label> <button class="btn btn-primary" id="phraseContinueBtn" disabled data-act="call" data-fn="confirmGeneratedPhrase" style="width: 100%; margin-top: var(--spacing-md);">Continue</button> </div> </div> <!-- Seed Phrase Input Screen (User-Provided) --> <div class="screen" id="seedPhraseScreen"> <div class="screen-header"> <h1>Enter Your Seed Phrase</h1> <p>Paste or type your 12 words</p> </div> <div class="screen-content"> <div class="card" style="background: rgba(184, 134, 59, 0.1); border-color: rgba(184, 134, 59, 0.3); margin-bottom: var(--spacing-lg);"> <div style="display: flex; align-items: flex-start; gap: var(--spacing-md);"> <div style="color: var(--fire-orange); font-weight: 600;">Critical</div> <div style="color: var(--text-secondary); font-size: 13px;">Enter your 12-word recovery phrase. This is your access key — never share it with anyone.</div> </div> </div> <div class="form-group"> <label class="form-label">Your 12-Word Seed Phrase</label> <textarea id="seedPhraseInput" class="form-input" placeholder="word1 word2 word3 ... word12" style="height: 80px; resize: none; font-family: var(--font-data);" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea> <div style="color: var(--text-secondary); font-size: 12px; margin-top: var(--spacing-sm);">Enter your 12 words separated by spaces</div> </div> <div id="seedError" style="color: var(--danger); font-size: 13px; margin-top: var(--spacing-md); display: none;"></div> <button class="btn btn-primary" data-act="call" data-fn="verifySeedPhrase" style="width: 100%; margin-top: var(--spacing-lg);">Verify Seed Phrase →</button> <button class="btn btn-secondary" data-act="screen" data-arg="walletSetupScreen" style="width: 100%;">Back</button> </div> </div> <!-- Import From Seed Screen --> <div class="screen" id="importSeedScreen"> <div class="screen-header"> <h1>Import From Seed</h1> <p>Recover your wallet using seed phrase</p> </div> <div class="screen-content"> <div class="form-group"> <label class="form-label">Seed Phrase</label> <textarea class="form-input" placeholder="mineral swift option stale harbor pearl space bench payment bomb hail motes" style="height: 100px; resize: none;" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-lpignore="true" data-1p-ignore></textarea> <div style="color: var(--text-secondary); font-size: 12px; margin-top: var(--spacing-sm);">Enter your 12 or 24-word seed phrase</div> </div> <div class="form-group"> <label class="form-label">Wallet Name</label> <input type="text" class="form-input" placeholder="e.g., My Imported Wallet"> </div> <button class="btn btn-primary" data-act="screen" data-arg="passwordScreen" style="width: 100%;">Continue →</button> <button class="btn btn-secondary" data-act="screen" data-arg="walletSetupScreen" style="width: 100%; margin-top: var(--spacing-md);">Back</button> </div> </div> <!-- PIN Recovery Screen (Forgot PIN) --> <div class="screen" id="pinRecoveryScreen"> <div class="screen-header"> <h1>Recover Your PIN</h1> <p>Use your seed phrase to reset access</p> </div> <div class="screen-content"> <div class="card" style="background: rgba(184, 134, 59, 0.1); border-color: rgba(184, 134, 59, 0.3); margin-bottom: var(--spacing-lg);"> <div style="color: var(--text-secondary); font-size: 13px;">Enter your 12-word recovery phrase to reset your PIN.</div> </div> <div class="form-group"> <label class="form-label">Your 9-Word Seed Phrase</label> <textarea id="recoveryPhraseInput" class="form-input" placeholder="word1 word2 word3 word4 word5 word6 word7 word8 word9" style="height: 80px; resize: none; font-family: var(--font-data);" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea> <div style="color: var(--text-secondary); font-size: 12px; margin-top: var(--spacing-sm);">Enter your 12 words separated by spaces</div> </div> <div id="recoveryError" style="color: var(--danger); font-size: 13px; margin-top: var(--spacing-md); display: none;"></div> <button class="btn btn-primary" data-act="call" data-fn="verifyRecoverySeed" style="width: 100%; margin-top: var(--spacing-lg);">Verify Seed →</button> <button class="btn btn-secondary" data-act="screen" data-arg="loginScreen" style="width: 100%;">Back</button> </div> </div> <!-- New PIN Setup Screen (After Recovery) --> <div class="screen" id="newPINSetupScreen"> <div class="screen-header"> <h1>Create New PIN</h1> <p>Set a 6-digit access code</p> </div> <div class="screen-content"> <div class="form-group" style="text-align: center;"> <label class="form-label">Create a new 6-digit PIN</label> <div style="display: flex; gap: 8px; justify-content: center; margin: var(--spacing-lg) 0;"> <input type="text" maxlength="1" class="pin-input" style="width: 50px; height: 50px; text-align: center; font-size: 24px; border: 2px solid var(--border-color); border-radius: 8px; background: rgba(26, 77, 122, 0.1); color: var(--gold);" id="newPin0" data-act="pinfocus" data-arg="newPin1"> <input type="text" maxlength="1" class="pin-input" style="width: 50px; height: 50px; text-align: center; font-size: 24px; border: 2px solid var(--border-color); border-radius: 8px; background: rgba(26, 77, 122, 0.1); color: var(--gold);" id="newPin1" data-act="pinfocus" data-arg="newPin2"> <input type="text" maxlength="1" class="pin-input" style="width: 50px; height: 50px; text-align: center; font-size: 24px; border: 2px solid var(--border-color); border-radius: 8px; background: rgba(26, 77, 122, 0.1); color: var(--gold);" id="newPin2" data-act="pinfocus" data-arg="newPin3"> <input type="text" maxlength="1" class="pin-input" style="width: 50px; height: 50px; text-align: center; font-size: 24px; border: 2px solid var(--border-color); border-radius: 8px; background: rgba(26, 77, 122, 0.1); color: var(--gold);" id="newPin3" data-act="pinfocus" data-arg="newPin4"> <input type="text" maxlength="1" class="pin-input" style="width: 50px; height: 50px; text-align: center; font-size: 24px; border: 2px solid var(--border-color); border-radius: 8px; background: rgba(26, 77, 122, 0.1); color: var(--gold);" id="newPin4" data-act="pinfocus" data-arg="newPin5"> <input type="text" maxlength="1" class="pin-input" style="width: 50px; height: 50px; text-align: center; font-size: 24px; border: 2px solid var(--border-color); border-radius: 8px; background: rgba(26, 77, 122, 0.1); color: var(--gold);" id="newPin5" data-act="call" data-fn="handleNewPinComplete"> </div> </div> <button class="btn btn-primary" data-act="call" data-fn="saveRecoveredPIN" style="width: 100%; margin-top: var(--spacing-lg);">Save New PIN →</button> </div> </div> <!-- Secure Your Wallet Screen --> <div class="screen" id="secureWalletScreen"> <div class="screen-header"> <h1>Secure Your Wallet</h1> <p>Protect your funds with security setup</p> </div> <div class="screen-content" style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--spacing-lg);"> <div style="width: 180px; height: 180px; background: linear-gradient(135deg, #2E9E6B, #25845A); border-radius: 24px; display: flex; align-items: center; justify-content: center; font-size: 100px; box-shadow: 0 10px 40px rgba(34, 197, 94, 0.3);"></div> <div style="text-align: center;"> <h3 style="color: var(--text-primary); margin-bottom: var(--spacing-sm);">Secure Your Wallet</h3> <p style="color: var(--text-secondary); font-size: 14px; line-height: 1.6; margin-bottom: var(--spacing-lg);">Don't risk losing your funds. Protect your wallet by saving your seed phrase in a place you trust.</p> <div style="background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3); border-radius: 12px; padding: var(--spacing-md); margin-bottom: var(--spacing-md);"> <p style="color: var(--text-secondary); font-size: 13px; line-height: 1.6;">It's the only way to recover your wallet if you get locked out of the app or get a new device.</p> </div> </div> <button class="btn btn-primary" data-act="screen" data-arg="seedPhraseScreen" style="width: 100%; max-width: 300px;">Start</button> <button class="btn btn-secondary" data-act="screen" data-arg="passwordScreen" style="width: 100%; max-width: 300px;">Remind me later</button> </div> </div> <!-- Password Setup Screen - Enhanced --> <div class="screen" id="passwordScreen"> <div class="screen-header"> <h1>Establish Access Control</h1> <p>This password will unlock your MoonBite wallet only on this device.</p> </div> <div class="screen-content"> <div class="form-group"> <label class="form-label">New Password</label> <div style="position: relative;"> <input type="password" autocomplete="off" data-lpignore="true" data-1p-ignore class="form-input" placeholder="Password" id="passwordInput" data-act="call" data-fn="updatePasswordStrength"> <button style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 18px;" data-act="call" data-fn="togglePasswordVisibility" data-arg="passwordInput"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button> </div> <div style="color: var(--text-secondary); font-size: 12px; margin-top: var(--spacing-sm);">Must be at least 8 characters</div> </div> <div class="form-group"> <label class="form-label">Confirm New Password</label> <div style="position: relative;"> <input type="password" autocomplete="off" data-lpignore="true" data-1p-ignore class="form-input" placeholder="Password" id="confirmPasswordInput"> <button style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 18px;" data-act="call" data-fn="togglePasswordVisibility" data-arg="confirmPasswordInput"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></button> </div> </div> <div class="card"> <div class="card-title">Password Strength</div> <div style="display: flex; gap: var(--spacing-sm); margin-top: var(--spacing-md);"> <div id="strengthBar1" style="flex: 1; height: 8px; background: var(--success); border-radius: 4px;"></div> <div id="strengthBar2" style="flex: 1; height: 8px; background: rgba(74, 222, 128, 0.3); border-radius: 4px;"></div> <div id="strengthBar3" style="flex: 1; height: 8px; background: rgba(74, 222, 128, 0.3); border-radius: 4px;"></div> <div id="strengthBar4" style="flex: 1; height: 8px; background: rgba(74, 222, 128, 0.3); border-radius: 4px;"></div> </div> </div> <div class="card"> <div class="card-title">Password Requirements</div> <div style="margin-top: var(--spacing-md);"> <div style="display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-sm); color: var(--success);"> <span><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg></span> At least 8 characters </div> <div style="display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-sm); color: var(--success);"> <span><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg></span> Contains uppercase letter </div> <div style="display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-sm); color: var(--success);"> <span><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg></span> Contains number </div> <div style="display: flex; align-items: center; gap: var(--spacing-sm); color: var(--success);"> <span><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg></span> Contains special character </div> </div> </div> <div class="card" style="background: rgba(74, 222, 128, 0.05); border-color: rgba(74, 222, 128, 0.2); margin-top: var(--spacing-lg);"> <label style="display: flex; align-items: flex-start; gap: var(--spacing-md); cursor: pointer;"> <input type="checkbox" style="width: 18px; height: 18px; cursor: pointer; margin-top: 2px;" required> <div style="font-size: 13px; color: var(--text-secondary);">I understand that MoonBite cannot recover this password for me. <span style="color: var(--success);">Learn more</span></div> </label> </div> <button class="btn btn-primary" data-act="screen" data-arg="secureWalletScreen" style="width: 100%; margin-top: var(--spacing-lg);">Create Password</button> </div> </div> <!-- Write Seed Phrase Screen --> <div class="screen" id="writeSeedScreen"> <div class="screen-header"> <h1>Write Down Seed Phrase</h1> <p>Write these 12 words in the same order in a safe place. You will need these words to restore your wallet.</p> </div> <div class="screen-content"> <div class="card" style="background: rgba(74, 222, 128, 0.1); border-color: rgba(74, 222, 128, 0.3);"> <div style="display: flex; align-items: flex-start; gap: var(--spacing-md);"> <span style="font-size: 20px;"></span> <div> <div style="color: var(--success); font-weight: 600; margin-bottom: var(--spacing-sm);">Tip to never lose your seed phrase</div> <div style="color: var(--text-secondary); font-size: 12px;">Write down all 12 words and keep them in a safe place. You can also store a backup in a secure location.</div> </div> </div> </div> <div class="card" style="margin-top: var(--spacing-lg);"> <div class="card-title">12-Word Seed Phrase</div> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-md); margin-top: var(--spacing-md);"> <div style="padding: var(--spacing-sm); background: rgba(26, 77, 122, 0.2); border: 1px solid rgba(74, 222, 128, 0.3); border-radius: 8px;"><span style="color: var(--text-secondary); font-size: 12px;">1.</span> mineral</div> <div style="padding: var(--spacing-sm); background: rgba(26, 77, 122, 0.2); border: 1px solid rgba(74, 222, 128, 0.3); border-radius: 8px;"><span style="color: var(--text-secondary); font-size: 12px;">2.</span> swift</div> <div style="padding: var(--spacing-sm); background: rgba(26, 77, 122, 0.2); border: 1px solid rgba(74, 222, 128, 0.3); border-radius: 8px;"><span style="color: var(--text-secondary); font-size: 12px;">3.</span> option</div> <div style="padding: var(--spacing-sm); background: rgba(26, 77, 122, 0.2); border: 1px solid rgba(74, 222, 128, 0.3); border-radius: 8px;"><span style="color: var(--text-secondary); font-size: 12px;">4.</span> stale</div> <div style="padding: var(--spacing-sm); background: rgba(26, 77, 122, 0.2); border: 1px solid rgba(74, 222, 128, 0.3); border-radius: 8px;"><span style="color: var(--text-secondary); font-size: 12px;">5.</span> harbor</div> <div style="padding: var(--spacing-sm); background: rgba(26, 77, 122, 0.2); border: 1px solid rgba(74, 222, 128, 0.3); border-radius: 8px;"><span style="color: var(--text-secondary); font-size: 12px;">6.</span> pearl</div> <div style="padding: var(--spacing-sm); background: rgba(26, 77, 122, 0.2); border: 1px solid rgba(74, 222, 128, 0.3); border-radius: 8px;"><span style="color: var(--text-secondary); font-size: 12px;">7.</span> space</div> <div style="padding: var(--spacing-sm); background: rgba(26, 77, 122, 0.2); border: 1px solid rgba(74, 222, 128, 0.3); border-radius: 8px;"><span style="color: var(--text-secondary); font-size: 12px;">8.</span> bench</div> <div style="padding: var(--spacing-sm); background: rgba(26, 77, 122, 0.2); border: 1px solid rgba(74, 222, 128, 0.3); border-radius: 8px;"><span style="color: var(--text-secondary); font-size: 12px;">9.</span> payment</div> <div style="padding: var(--spacing-sm); background: rgba(26, 77, 122, 0.2); border: 1px solid rgba(74, 222, 128, 0.3); border-radius: 8px;"><span style="color: var(--text-secondary); font-size: 12px;">10.</span> bomb</div> <div style="padding: var(--spacing-sm); background: rgba(26, 77, 122, 0.2); border: 1px solid rgba(74, 222, 128, 0.3); border-radius: 8px;"><span style="color: var(--text-secondary); font-size: 12px;">11.</span> hail</div> <div style="padding: var(--spacing-sm); background: rgba(26, 77, 122, 0.2); border: 1px solid rgba(74, 222, 128, 0.3); border-radius: 8px;"><span style="color: var(--text-secondary); font-size: 12px;">12.</span> motes</div> </div> </div> <button class="btn btn-primary" data-act="screen" data-arg="confirmSeedScreen" style="width: 100%; margin-top: var(--spacing-lg);">Next</button> </div> </div> <!-- Confirm Seed Phrase Screen --> <div class="screen" id="confirmSeedScreen"> <div class="screen-header"> <h1>Verify Seed Phrase</h1> <p>Select the correct words to confirm you've secured your phrase.</p> </div> <div class="screen-content"> <div style="background: linear-gradient(135deg, rgba(217, 164, 65, 0.08), rgba(217, 164, 65, 0.02)); border: 1px solid rgba(217, 164, 65, 0.12); border-radius: 12px; padding: var(--spacing-md); margin-bottom: var(--spacing-lg);"> <div style="font-size: 13px; color: var(--text-secondary); letter-spacing: 0.2px;">WORD TO CONFIRM: <span style="color: var(--primary); font-weight: 600; font-family: var(--font-data);">6</span></div> </div> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-md); margin-bottom: var(--spacing-lg);"> <button style="padding: var(--spacing-md); background: rgba(217, 164, 65, 0.04); border: 1px solid rgba(217, 164, 65, 0.12); border-radius: 8px; color: var(--text-primary); cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s;">mineral</button> <button style="padding: var(--spacing-md); background: rgba(217, 164, 65, 0.04); border: 1px solid rgba(217, 164, 65, 0.12); border-radius: 8px; color: var(--text-primary); cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s;">space</button> <button style="padding: var(--spacing-md); background: rgba(217, 164, 65, 0.04); border: 1px solid rgba(217, 164, 65, 0.12); border-radius: 8px; color: var(--text-primary); cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s;">pearl</button> <button style="padding: var(--spacing-md); background: rgba(217, 164, 65, 0.04); border: 1px solid rgba(217, 164, 65, 0.12); border-radius: 8px; color: var(--text-primary); cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s;">bench</button> </div> <button class="btn btn-primary" data-act="screen" data-arg="pinSetupScreen" style="width: 100%;">Seed Verified — Proceed to PIN</button> </div> </div> <!-- Seed Phrase Info Screen --> <div class="screen" id="seedInfoScreen"> <div class="screen-header"> <h1>Recovery Phrase Explained</h1> <p>Understanding your wallet recovery key</p> </div> <div class="screen-content"> <div class="card"> <div style="color: var(--text-primary); font-weight: 600; margin-bottom: var(--spacing-md);">Definition</div> <div style="color: var(--text-secondary); font-size: 13px; line-height: 1.6;">A seed phrase is a set of twelve words that contains all the information about your wallet, including your funds. It is a secret code used to access your assets.</div> </div> <div class="card"> <div style="color: var(--text-primary); font-weight: 600; margin-bottom: var(--spacing-md);">Security</div> <div style="color: var(--text-secondary); font-size: 13px; line-height: 1.6;">You must keep your seed phrase secret and safe. If someone gets your seed phrase, they will gain control over your accounts.</div> </div> <div class="card"> <div style="color: var(--text-primary); font-weight: 600; margin-bottom: var(--spacing-md);">Recovery</div> <div style="color: var(--text-secondary); font-size: 13px; line-height: 1.6;">Save it in a place where you can access it. If you lose it, not even MoonBite can help you recover it. It is the only way to access your wallet if you get locked out of the app or get a new device.</div> </div> <button class="btn btn-primary" data-act="screen" data-arg="skipSecurityScreen" style="width: 100%; margin-top: var(--spacing-lg);">OK, I Got It</button> </div> </div> <!-- Skip Account Security Screen --> <div class="screen" id="skipSecurityScreen"> <div class="screen-header"> <h1>Skip Account Security?</h1> <p>Confirm wallet setup</p> </div> <div class="screen-content" style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--spacing-lg);"> <div style="width: 180px; height: 180px; background: linear-gradient(135deg, #2E9E6B, #25845A); border-radius: 24px; display: flex; align-items: center; justify-content: center; font-size: 100px; box-shadow: 0 10px 40px rgba(34, 197, 94, 0.3);"></div> <div style="text-align: center;"> <h3 style="color: var(--text-primary); margin-bottom: var(--spacing-sm);">Successful</h3> <p style="color: var(--text-secondary); font-size: 14px; line-height: 1.6; margin-bottom: var(--spacing-lg);">Your wallet has been created. Make sure to save your seed phrase to keep your wallet accessible and protected.</p> </div> <div style="background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3); border-radius: 12px; padding: var(--spacing-md); width: 100%;"> <label style="display: flex; align-items: flex-start; gap: var(--spacing-md); cursor: pointer;"> <input type="checkbox" style="width: 18px; height: 18px; cursor: pointer; margin-top: 2px;"> <div style="font-size: 13px; color: var(--text-secondary);">I understand that if I lose my seed phrase I will not be able to access my wallet.</div> </label> </div> <button class="btn btn-primary" data-act="call" data-fn="completeOnboarding" style="width: 100%; max-width: 300px;">Complete Setup</button> </div> </div> <!-- Dashboard Screen --> <div class="screen" id="dashboardScreen"> <div class="screen-content dash-content"> <!-- Shown only when this wallet's seed was stored by a build with the broken PIN encryption. The seed was recoverable from this device without the PIN while that build was installed, so re-encrypting it is not enough — the seed itself may already be known, and the only real remedy is to move to a fresh one. --> <div id="seedRotationWarning" style="display:none; margin-bottom: var(--spacing-md); padding: var(--spacing-md); border: 1px solid var(--danger, #C0392B); border-radius: 12px; background: rgba(192,57,67,0.08);"> <div style="font-size: .62rem; letter-spacing: .18em; text-transform: uppercase; color: var(--danger, #C0392B); font-weight: 700; margin-bottom: 6px;">Security Notice</div> <div style="font-size: 13px; line-height: 1.5; color: var(--text-primary);"> This wallet's recovery phrase was saved by an earlier version that stored it insecurely, so it should be treated as exposed. To stay safe, create a new wallet with a fresh phrase and move any balance to it. </div> <div style="display:flex; gap: var(--spacing-sm); margin-top: var(--spacing-md);"> <button class="btn btn-primary" style="flex:1;" data-act="call" data-fn="beginSeedRotation">Create New Wallet</button> <button class="btn btn-secondary" data-act="call" data-fn="acknowledgeSeedExposure" title="Dismiss this notice">I understand</button> </div> </div> <div class="install-prompt" id="installPrompt"> <div class="install-prompt-title">Install MoonBite Wallet</div> <div class="install-prompt-text">Fast, secure, offline access</div> <div class="install-prompt-buttons"> <button class="install-prompt-btn" id="installBtn">Install</button> <button class="install-prompt-btn" id="dismissBtn">Later</button> </div> </div> <!-- EMPTY STATE: No balance yet --> <div id="emptyStateCard" class="empty-state-card" style="display:none;"> <div class="empty-hero"> <svg class="empty-icon-svg" viewBox="0 0 100 100" fill="none" stroke="var(--primary)" stroke-width="2"> <circle cx="50" cy="50" r="45"/> <path d="M50 25v50M35 50h30" stroke-width="1.5"/> </svg> <h2>Welcome to MoonBite</h2> <p class="empty-subtitle">You don't have any MBITE yet.</p> </div> <div class="empty-paths"> <div class="path-card"> <div class="path-title">1. Mine MBITE</div> <p class="path-desc">Run a RandomX miner on your CPU to earn blocks.</p> <a href="https://moonbite.org/start" target="_blank" class="path-link">Mining Guide →</a> </div> <div class="path-card"> <div class="path-title">2. Receive from Others</div> <p class="path-desc">Someone sends you MBITE to your wallet address.</p> <button data-act="screen" data-arg="receiveScreen" class="path-link">Show My Address →</button> </div> <div class="path-card"> <div class="path-title">3. Testnet Sandbox</div> <p class="path-desc">Mine on testnet to learn with free MBITE.</p> <a href="https://moonbite.org/classic" target="_blank" class="path-link">Switch Network →</a> </div> </div> <div class="empty-address-box"> <div class="addr-label">Your Mainnet Address</div> <div class="addr-display mono" id="emptyAddrDisplay">moon1...</div> <button data-act="copyel" data-arg="emptyAddrDisplay" class="addr-copy-btn">Copy Address</button> </div> </div> <!-- BALANCE STATE: User has MBITE --> <div id="balanceStateCard" class="balance-state-card" style="display:none;"> <!-- The vault card --> <div class="premium-balance-card"> <div class="vault-top"> <div class="vault-brand"> <svg class="brand-mark" viewBox="-140 -125 280 250" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M 13.99 -99.02 A 100 100 0 1 0 99.02 -13.99 A 62.00 62.00 0 0 1 13.99 -99.02 Z" fill="#F5F2EA"/><rect x="-127.5" y="-34" width="255" height="13" rx="6.5" fill="#D9A441"/><rect x="-127.5" y="18" width="255" height="13" rx="6.5" fill="#D9A441"/></svg> <span>MOONBITE</span> </div> <div class="vault-net">MAINNET</div> </div> <div class="vault-mid"> <div class="balance-label">Total Balance</div> <div class="balance-amount"><span id="balanceAmount">0.0000</span><svg class="mbite-glyph" viewBox="-140 -125 280 250" aria-label="MBITE" role="img"><path d="M 13.99 -99.02 A 100 100 0 1 0 99.02 -13.99 A 62.00 62.00 0 0 1 13.99 -99.02 Z" fill="currentColor"/><rect x="-127.5" y="-34" width="255" height="13" rx="6.5" fill="#D9A441"/><rect x="-127.5" y="18" width="255" height="13" rx="6.5" fill="#D9A441"/></svg></div> <div class="vault-sub" id="vaultSub">Earned through mining</div> </div> <div class="vault-bottom"> <div class="vault-holder"> <div class="vault-k">Wallet</div> <div class="vault-v" id="walletName">WALLET 1</div> </div> <div class="vault-holder" style="text-align:right;"> <div class="vault-k">Blocks to Halving</div> <div class="vault-v num" id="blocksRemaining">1650000</div> </div> </div> </div> <div class="premium-wallet-card"> <div class="wallet-icon"> <svg viewBox="0 0 24 24" fill="var(--primary)" opacity="0.9"> <path d="M12 2L3 7v10c0 7 9 9 9 9s9-2 9-9V7l-9-5z"/> </svg> </div> <div class="wallet-details"> <div class="wallet-label">Primary Wallet</div> <div class="wallet-name-display" id="walletNameDisplay">MoonBite Wallet 1</div> <div class="wallet-amount" id="walletAmount">0.0000 MBITE</div> </div> </div> <div class="action-buttons"> <button class="action-btn" data-act="screen" data-arg="activityScreen"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M3 12h4l2 5 4-12 2 7h6"/> </svg> <span>Activity</span> </button> <button class="action-btn" data-act="screen" data-arg="receiveScreen"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M17 7L7 17M17 17H7V7"/> </svg> <span>Receive</span> </button> <button class="action-btn" data-act="screen" data-arg="sendScreen"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M7 17L17 7M7 7h10v10"/> </svg> <span>Send</span> </button> </div> </div> </div> </div> <!-- Wallets Screen --> <div class="screen" id="walletsScreen"> <div class="screen-header"> <h1>My Wallet</h1> <p>Your active wallet</p> </div> <div class="screen-content" id="walletsContainer"> <!-- Dynamically populated with user's actual wallet --> </div> </div> <!-- QR Code Scanner Screen --> <div class="screen" id="qrScannerScreen"> <div class="screen-header"> <h1>Scan QR Code</h1> <p>Point your camera at the QR Code</p> </div> <div class="screen-content" style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--spacing-lg);"> <div style="width: 220px; height: 220px; background: linear-gradient(135deg, rgba(26, 77, 122, 0.3), rgba(184, 134, 59, 0.2)); border: 3px dashed var(--fire-orange); border-radius: 16px; display: flex; align-items: center; justify-content: center;"> <div style="text-align: center;"> <div style="margin-bottom: var(--spacing-md); color: var(--text-secondary);"><svg viewBox="0 0 24 24" width="56" height="56" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"/><circle cx="12" cy="13" r="4"/></svg></div> <p style="color: var(--text-secondary); font-size: 13px;">Position QR code in frame</p> </div> </div> <div style="display: flex; gap: var(--spacing-lg); width: 100%; justify-content: center;"> <button style="width: 50px; height: 50px; border-radius: 50%; background: var(--fire-orange); border: none; color: white; font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center;">●</button> </div> <button class="btn btn-secondary" data-act="screen" data-arg="sendScreen" style="width: 100%;">← Cancel</button> </div> </div> <!-- Send Screen - Enhanced --> <div class="screen" id="sendScreen"> <div class="screen-header"> <h1>Send MBITE</h1> <p>Transfer cryptocurrency to another wallet</p> </div> <div class="screen-content"> <div class="form-group"> <label class="form-label">Recipient Address</label> <div style="display: flex; gap: var(--spacing-sm);"> <input type="text" class="form-input" placeholder="Enter MBITE address or scan QR" id="sendRecipient" style="flex: 1;"> <button style="background: var(--fire-orange); color: white; border: none; padding: 0 var(--spacing-md); min-width: 44px; min-height: 44px; border-radius: 8px; cursor: pointer; font-size: 18px;" data-act="screen" data-arg="qrScannerScreen" title="Scan QR code" aria-label="Scan QR code"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 14h3v3h-3zM19 19h2M14 19v2"/></svg></button> </div> </div> <div class="form-group"> <label class="form-label">Amount (MBITE)</label> <div style="display:flex; gap: var(--spacing-sm); align-items:center;"> <input type="number" class="form-input" placeholder="0.00" id="sendAmount" style="font-size: 18px; flex:1;"> <button type="button" data-act="call" data-fn="setMaxSendAmount" class="btn btn-secondary" style="width:auto; padding: 0 var(--spacing-md); min-height:44px; margin:0;">Max</button> </div> <div id="sendAvailable" style="font-size: 12px; color: var(--text-secondary); margin-top: 6px;"></div> <div id="sendReviewError" style="font-size: 13px; color: var(--danger); margin-top: 8px; min-height: 16px;"></div> </div> <div class="card"> <div class="card-title">Network Fee Priority</div> <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: var(--spacing-sm); margin-top: var(--spacing-md);"> <button class="btn btn-small" data-act="feespeed" data-arg="FAST" style="background: linear-gradient(135deg, var(--fire-orange), #B8863B); color: white;">FAST</button> <button class="btn btn-small" data-act="feespeed" data-arg="STANDARD" style="background: rgba(26, 77, 122, 0.2); color: var(--gold);">STANDARD</button> <button class="btn btn-small" data-act="feespeed" data-arg="SLOW" style="background: rgba(26, 77, 122, 0.2); color: var(--gold);">SLOW</button> </div> </div> <div class="card"> <div style="display: flex; justify-content: space-between; align-items: center;"> <div> <div class="card-title">Network Fee</div> <div class="card-value" id="networkFeeDisplay">0.00002 MBITE</div> </div> <button style="background: var(--fire-orange); color: white; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 12px;" data-act="screen" data-arg="networkFeeScreen">Edit</button> </div> <div style="border-top: 1px solid var(--border-color); margin-top: var(--spacing-md); padding-top: var(--spacing-md);"> <div class="card-title">Total Amount</div> <div class="card-value" id="totalAmountDisplay">1.50002 MBITE</div> </div> </div> <button class="btn btn-primary" data-act="call" data-fn="reviewAndSend">Review & Send</button> <button class="btn btn-secondary" data-act="screen" data-arg="dashboardScreen">← Back</button> </div> </div> <!-- Network Fee Editor Screen --> <div class="screen" id="networkFeeScreen"> <div class="screen-header"> <h1>Edit Network Fee</h1> <p>Adjust the transaction fee</p> </div> <div class="screen-content"> <div class="card"> <div class="card-title">Current Speed</div> <div class="card-value" id="feeSpeedDisplay">Standard</div> </div> <div class="form-group"> <label class="form-label">Custom Fee (MBITE)</label> <input type="number" class="form-input" placeholder="0.0001" id="customFeeInput" value="0.0001" step="0.00001"> <div style="color: var(--text-secondary); font-size: 12px; margin-top: var(--spacing-sm);">Estimated time: ~2 minutes</div> </div> <div class="card" style="background: rgba(74, 222, 128, 0.05); border-color: rgba(74, 222, 128, 0.2);"> <div class="card-title">Fee Recommendations</div> <div style="margin-top: var(--spacing-md); display: flex; flex-direction: column; gap: var(--spacing-sm);"> <div style="display: flex; justify-content: space-between; font-size: 13px;"> <span style="color: var(--text-secondary);">Fast (2-5 min)</span> <span style="color: var(--gold); cursor: pointer;" data-act="setfee" data-arg="0.0005">0.0005</span> </div> <div style="display: flex; justify-content: space-between; font-size: 13px;"> <span style="color: var(--text-secondary);">Standard (10-15 min)</span> <span style="color: var(--gold); cursor: pointer;" data-act="setfee" data-arg="0.0001">0.0001</span> </div> <div style="display: flex; justify-content: space-between; font-size: 13px;"> <span style="color: var(--text-secondary);">Slow (30+ min)</span> <span style="color: var(--gold); cursor: pointer;" data-act="setfee" data-arg="0.00005">0.00005</span> </div> </div> </div> <button class="btn btn-primary" data-act="screen" data-arg="sendScreen">Confirm Fee</button> </div> </div> <!-- Transaction Confirmation Screen --> <div class="screen" id="confirmSendScreen"> <div class="screen-header"> <h1>Confirm Transaction</h1> <p>Review transaction details</p> </div> <div class="screen-content"> <div class="card"> <div class="card-title">Recipient</div> <div class="card-value" id="confirmRecipient" style="font-size: 14px; word-break: break-all;">-</div> </div> <div class="card"> <div style="display: flex; justify-content: space-between; margin-bottom: var(--spacing-md);"> <div> <div class="card-title">Amount</div> <div class="card-value" id="confirmAmount" style="font-size: 20px;">-</div> </div> </div> </div> <div class="card"> <div style="display: flex; justify-content: space-between; margin-bottom: var(--spacing-sm);"> <div class="card-title">Network Fee</div> <div class="card-value" id="confirmFee" style="font-size: 14px;">-</div> </div> <div style="border-top: 1px solid var(--border-color); padding-top: var(--spacing-md); margin-top: var(--spacing-md); display: flex; justify-content: space-between;"> <div class="card-title">Total</div> <div class="card-value" id="confirmTotal" style="font-size: 18px;">-</div> </div> </div> <div class="card" style="border-color: rgba(200,64,47,.4);"> <div class="card-title" style="color: var(--danger);">THIS CANNOT BE UNDONE</div> <p style="color: var(--text-secondary); font-size: 13px; line-height: 1.6; margin: 8px 0 12px;"> A sent transaction cannot be recalled, reversed or refunded by anyone, including us. Type the amount below to confirm you have checked it.</p> <input type="text" class="form-input" id="sendConfirmInput" inputmode="decimal" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-lpignore="true" data-1p-ignore placeholder="Type the amount, e.g. 1.5" aria-label="Type the amount to confirm" style="font-family: ui-monospace, SFMono-Regular, Menlo, monospace;"> <div id="sendConfirmHint" style="font-size: 12px; color: var(--text-tertiary); margin-top: 8px;"> Must match <span id="sendConfirmTarget" style="font-family: ui-monospace, Menlo, monospace; color: var(--text-primary);">1.5</span> exactly.</div> </div> <button class="btn btn-primary" id="sendConfirmBtn" disabled data-act="call" data-fn="sendTransaction" style="background: var(--danger); opacity:.45; cursor:not-allowed;">Confirm & Send</button> <button class="btn btn-secondary" data-act="screen" data-arg="sendScreen">← Edit Transaction</button> </div> </div> <!-- Transaction Success Screen --> <div class="screen" id="transactionSuccessScreen"> <div class="screen-header"> <h1>Transaction Sent</h1> <!-- Broadcast is not confirmation: it waits in the mempool until a miner includes it in a block. Updated in place by pollSuccessConfirmation() once it lands. --> <p id="successStatusLine">Waiting to be included in a block</p> </div> <div class="screen-content" style="display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--spacing-lg);"> <div style="width: 120px; height: 120px; background: linear-gradient(135deg, #2E9E6B, #25845A); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; animation: pulse 1s ease-out;"><svg viewBox="0 0 24 24" width="56" height="56" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg></div> <div style="text-align: center;"> <h3 style="color: var(--gold); margin-bottom: var(--spacing-sm);"><span id="successAmount">0</span> MBITE Sent</h3> <p style="color: var(--text-secondary); font-size: 14px; margin-bottom: var(--spacing-lg);">Your transaction has been broadcast to the network</p> </div> <div class="card"> <div class="card-title">Transaction Hash</div> <div id="successTxid" style="color: var(--gold); font-family: monospace; font-size: 12px; word-break: break-all; margin-top: var(--spacing-sm);">—</div> </div> <button class="btn btn-primary" data-act="screen" data-arg="dashboardScreen" style="background: #2E9E6B;">Back to Dashboard</button> <button class="btn btn-secondary" data-act="screen" data-arg="sendScreen">Send More</button> </div> </div> <!-- Receive Screen - PRODUCTION GOD LEVEL --> <div class="screen" id="receiveScreen"> <div class="screen-header"> <h1>Receive MBITE</h1> <p>Mining-Ready Wallet Address</p> </div> <div class="screen-content" style="display: flex; flex-direction: column; gap: var(--spacing-md);"> <!-- BLOCKCHAIN STATUS --> <div style="background: linear-gradient(135deg, rgba(26, 31, 58, 0.6) 0%, rgba(21, 26, 47, 0.4) 100%); border: 1px solid rgba(217, 164, 65, 0.12); border-radius: 12px; padding: 14px; display: grid; grid-template-columns: 1fr 1fr; gap: 12px;"> <div> <div style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: #888; margin-bottom: 4px;">Blockchain Height</div> <div style="font-size: 20px; font-weight: 700; color: var(--gold);" id="blockchainHeight">0</div> </div> <div> <div style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: #888; margin-bottom: 4px;">Wallet Balance</div> <div style="font-size: 20px; font-weight: 700; color: var(--emerald);" id="walletBalance">10.0000 MBITE</div> </div> </div> <!-- PREMIUM QR CODE DISPLAY --> <div style="background: linear-gradient(135deg, rgba(26, 31, 58, 0.7) 0%, rgba(21, 26, 47, 0.5) 100%); border: 1px solid rgba(217, 164, 65, 0.15); border-radius: 16px; padding: 24px; display: flex; flex-direction: column; align-items: center; gap: 12px;"> <div style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: #888; margin-bottom: 8px;">Scan for Mining</div> <div id="qrCodeContainer" style="text-align: center; width: 160px; height: 160px; display: flex; align-items: center; justify-content: center; background: white; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.3);"></div> <div style="font-size: 11px; color: #888; text-align: center; margin-top: 8px;">Share this QR code with miners to receive MBITE</div> </div> <!-- WALLET ADDRESS - PREMIUM DISPLAY --> <div> <label class="form-label" style="margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px;">Your Mining Address</label> <div style="background: rgba(26, 31, 58, 0.5); border: 1px solid rgba(217, 164, 65, 0.12); border-radius: 12px; padding: 14px; display: flex; align-items: center; gap: 10px;"> <div id="receiveAddress" style="flex: 1; font-family: 'Courier New', monospace; font-size: 12px; color: var(--gold); word-break: break-all; font-weight: 600;">Unlock your wallet to reveal your address</div> <button data-act="call" data-fn="copyAddress" style="background: linear-gradient(135deg, var(--fire-orange), #B8863B); border: none; color: white; padding: 10px 14px; min-height: 44px; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; transition: all 0.3s ease; white-space: nowrap;">Copy</button> </div> <div id="copyFeedback" style="font-size: 11px; color: #2E9E6B; margin-top: 6px; text-align: center; display: none;">Copied to clipboard!</div> </div> <!-- WALLET INFO --> <div style="background: rgba(26, 31, 58, 0.4); border: 1px solid rgba(217, 164, 65, 0.08); border-radius: 12px; padding: 14px;"> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;"> <div> <div style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: #888; margin-bottom: 4px;">Wallet Name</div> <div style="font-size: 13px; font-weight: 600; color: #8A8580;">Primary Wallet</div> </div> <div> <div style="font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: #888; margin-bottom: 4px;">Status</div> <div style="font-size: 13px; font-weight: 600; color: var(--emerald);">Active</div> </div> </div> </div> <!-- ACTION BUTTONS --> <button class="btn btn-primary" data-act="call" data-fn="copyAddress" style="width: 100%; margin-top: 8px;">Copy Full Address</button> <button class="btn btn-secondary" data-act="call" data-fn="shareWalletAddress" style="width: 100%;">Share Mining Address</button> <button class="btn btn-secondary" data-act="screen" data-arg="dashboardScreen" style="width: 100%;">← Back to Dashboard</button> </div> </div> <!-- Security Screen --> <div class="screen" id="securityScreen"> <div class="screen-header"> <h1>Security</h1> <p>Protect your wallet</p> </div> <div class="screen-content"> <div class="form-group"> <label class="form-label">Auto-Lock Timeout</label> <div class="toggle"> <span>Lock after 5 minutes</span> <div class="toggle-switch"></div> </div> </div> <div class="card"> <div class="card-title">Recovery Options</div> <button class="btn btn-secondary" data-act="call" data-fn="revealRecoveryPhrase">View Recovery Phrase</button> <button class="btn btn-secondary" data-act="screen" data-arg="createPasswordScreen" style="margin-top: var(--spacing-sm);">Change Password</button> </div> <div class="card"> <div class="card-title">Hardware Wallet</div> <button class="btn btn-secondary">Connect Ledger</button> <button class="btn btn-secondary" style="margin-top: var(--spacing-sm);">Connect Trezor</button> </div> </div> </div> <!-- Contacts Screen --> <div class="screen" id="contactsScreen"> <div class="screen-header"> <h1>Contacts</h1> <p>Frequently used addresses</p> </div> <div class="screen-content"> <div class="form-group"> <input type="text" class="form-input" id="contactSearch" placeholder="Search contacts..." data-act="contactsearch"> </div> <!-- Populated from the user's own saved contacts. It used to ship three hardcoded demo entries, so every brand-new wallet looked like it already had an address book. --> <div id="contactsList"></div> <div id="contactsEmpty" style="text-align:center; padding: var(--spacing-lg) 0; color: var(--text-secondary); font-size: 13px; display:none;"> No contacts yet. Add an address you send to often. </div> <button class="btn btn-primary" data-act="screen" data-arg="addContactScreen">Add Contact</button> </div> </div> <!-- Create Password Screen --> <div class="screen" id="createPasswordScreen"> <div class="screen-header"> <h1>Create Password</h1> <p>Secure your wallet</p> </div> <div class="screen-content"> <div class="form-group"> <label class="form-label">Password</label> <input type="password" autocomplete="off" data-lpignore="true" data-1p-ignore class="form-input" placeholder="Enter password"> </div> <div class="form-group"> <label class="form-label">Confirm Password</label> <input type="password" autocomplete="off" data-lpignore="true" data-1p-ignore class="form-input" placeholder="Confirm password"> </div> <div class="card"> <div class="card-title">Password Strength</div> <div style="display: flex; gap: var(--spacing-sm); margin-top: var(--spacing-md);"> <div style="flex: 1; height: 8px; background: var(--success); border-radius: 4px;"></div> <div style="flex: 1; height: 8px; background: var(--success); border-radius: 4px;"></div> <div style="flex: 1; height: 8px; background: var(--success); border-radius: 4px;"></div> <div style="flex: 1; height: 8px; background: var(--success); border-radius: 4px;"></div> </div> </div> <div class="card"> <div class="card-title">Password Requirements</div> <div style="margin-top: var(--spacing-md);"> <div style="display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-sm); color: var(--success);"> <span><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg></span> <span>At least 8 characters</span> </div> <div style="display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-sm); color: var(--success);"> <span><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg></span> <span>Mix of uppercase and lowercase</span> </div> <div style="display: flex; align-items: center; gap: var(--spacing-sm); margin-bottom: var(--spacing-sm); color: var(--success);"> <span><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg></span> <span>At least one number</span> </div> <div style="display: flex; align-items: center; gap: var(--spacing-sm); color: var(--success);"> <span><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg></span> <span>At least one special character</span> </div> </div> </div> <button class="btn btn-primary" data-act="screen" data-arg="seedPhraseScreen">Create Password</button> </div> </div> <!-- Seed Phrase Screen --> <!-- duplicate seedPhraseScreen removed: shadowed by the first, unreachable --> <!-- Success Screen --> <div class="screen" id="successScreen"> <div class="screen-header"> <h1>Success!</h1> <p>Wallet ready to use</p> </div> <div class="screen-content" style="display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: var(--spacing-lg);"> <div class="success-icon"><svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6L9 17l-5-5"/></svg></div> <h2 style="color: var(--gold);">Wallet Created!</h2> <p style="color: var(--text-secondary);">Your MoonBite wallet is ready to use. You can now send, receive, and manage your MBITE coins.</p> <div class="card"> <span style="color: var(--success);">Wallet successfully created and secured</span> </div> <button class="btn btn-primary" data-act="screen" data-arg="dashboardScreen">Go to Dashboard</button> </div> </div> <!-- Create Wallet Screen --> <!-- duplicate createWalletScreen removed: shadowed by the first, unreachable --> <!-- Add Contact Screen --> <div class="screen" id="addContactScreen"> <div class="screen-header"> <h1>Add Contact</h1> <p>Save a frequently used address</p> </div> <div class="screen-content"> <div class="form-group"> <label class="form-label">Contact Name</label> <input type="text" class="form-input" id="newContactName" placeholder="e.g., John"> </div> <div class="form-group"> <label class="form-label">Wallet Address</label> <input type="text" class="form-input" id="newContactAddress" placeholder="Enter MBITE address"> </div> <button class="btn btn-primary" data-act="call" data-fn="saveContact">Save Contact</button> </div> </div> <!-- Confirm Send Screen --> <!-- duplicate confirmSendScreen removed: shadowed by the first, unreachable --> <!-- Activity / transaction history --> <div class="screen" id="activityScreen"> <div class="screen-header"> <h1>Activity</h1> <p>Your sends and receipts on this device</p> </div> <div class="screen-content"> <div id="activityList"></div> <div id="activityEmpty" style="text-align:center; padding: var(--spacing-lg) 0; color: var(--text-secondary); font-size: 13px; display:none;"> No activity yet. Sends and incoming MBITE will appear here. </div> </div> </div> <!-- Reveal recovery phrase (PIN-gated, from Security) --> <div class="screen" id="revealPhraseScreen"> <div class="screen-header"> <h1>Recovery Phrase</h1> <p>Write it down and keep it offline. Never share it.</p> </div> <div class="screen-content"> <div class="card" style="background: rgba(192,57,67,0.08); border-color: var(--danger,#C0392B); margin-bottom: var(--spacing-md);"> <div style="color: var(--text-secondary); font-size: 13px; line-height:1.5;"> Anyone with these words can take your funds. Make sure no one is watching your screen. </div> </div> <div id="revealedPhrase" style="font-family: var(--font-data, ui-monospace, monospace); font-size: 16px; line-height: 1.9; color: var(--gold); background: rgba(217,164,65,0.06); border: 1px solid rgba(217,164,65,0.18); border-radius: 12px; padding: var(--spacing-md); word-spacing: 6px; text-align: center;"></div> <button class="btn btn-primary" data-act="call" data-fn="hideRevealedPhrase" style="width:100%; margin-top: var(--spacing-lg);">Done</button> </div> </div> <!-- Confirm the newly generated phrase was written down --> <div class="screen" id="verifyBackupScreen"> <div class="screen-header"> <h1>Confirm Your Backup</h1> <p>Enter the requested words to prove you saved your phrase.</p> </div> <div class="screen-content"> <div id="verifyBackupPrompts"></div> <div id="verifyBackupError" style="color:#C8402F; font-size:13px; display:none; margin-top: var(--spacing-sm);">Those words do not match. Check your written copy and try again.</div> <button class="btn btn-primary" data-act="call" data-fn="checkPhraseBackup" style="width:100%; margin-top: var(--spacing-lg);">Confirm</button> <button class="btn btn-secondary" data-act="screen" data-arg="createWalletScreen" style="width:100%; margin-top: var(--spacing-sm);">Show My Phrase Again</button> </div> </div> </div> <!-- Theme Toggle Button --> <button class="theme-toggle" id="themeToggle" data-act="call" data-fn="toggleTheme" title="Toggle Theme" aria-label="Toggle theme"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg></button> <!-- Bottom Navigation --> <div class="bottom-nav"> <button class="nav-item active" data-act="screen-nav" data-arg="dashboardScreen" title="Home"> <div class="nav-icon"><svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12H3l9-9 9 9h-2"/><path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7"/><path d="M9 21v-6h6v6"/></svg></div> <div>Home</div> </button> <button class="nav-item" data-act="screen-nav" data-arg="walletsScreen" title="Wallet"> <div class="nav-icon"><svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17 8V6a2 2 0 0 0-2-2H5a2 2 0 0 0 0 4h14a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6"/><circle cx="16" cy="14" r="1.2"/></svg></div> <div>Wallet</div> </button> <button class="nav-item" data-act="screen-nav" data-arg="contactsScreen" title="Contacts"> <div class="nav-icon"><svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3h11a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><path d="M3 8h2M3 12h2M3 16h2"/><circle cx="12" cy="10" r="2.2"/><path d="M9 16a3 3 0 0 1 6 0"/></svg></div> <div>Contacts</div> </button> <button class="nav-item" data-act="screen-nav" data-arg="securityScreen" title="Security"> <div class="nav-icon"><svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l7 3v6c0 4.5-3 7.7-7 9-4-1.3-7-4.5-7-9V6z"/><circle cx="12" cy="12" r="1.6"/><path d="M12 13.6V16"/></svg></div> <div>Security</div> </button> </div> </div> <!-- PIN re-authorization, in-page. Authorizing a send and revealing the recovery phrase both used to call the browser-native prompt() for the PIN - which several real contexts refuse (installed-PWA standalone mode on many mobile platforms, some in-app webviews), silently blocking the wallet's core actions with no way to proceed. This modal replaces it: works everywhere, and matches the rest of the app. --> <div id="pinAuthOverlay" style="display:none;position:fixed;inset:0;z-index:500; background:rgba(11,13,18,0.72);backdrop-filter:blur(6px); align-items:center;justify-content:center;padding:20px;"> <div style="width:100%;max-width:340px;background:var(--bg-secondary,#161A22); border:1px solid var(--border-color,rgba(217,164,65,0.14));border-radius:16px; padding:28px 24px;text-align:center;"> <div id="pinAuthTitle" style="color:var(--text-primary,#F5F2EA);font-weight:700; font-size:16px;margin-bottom:6px;">Enter your PIN</div> <div id="pinAuthError" style="color:var(--danger,#C8402F);font-size:13px; min-height:18px;margin-bottom:10px;"></div> <div style="display:flex;gap:8px;justify-content:center;margin:10px 0 20px;"> <input type="password" inputmode="numeric" maxlength="1" class="pin-input pin-auth-box" style="width:42px;height:50px;text-align:center;font-size:22px; border:2px solid var(--border-color,rgba(217,164,65,0.14));border-radius:8px; background:rgba(217,164,65,0.06);color:var(--gold,#D9A441);"> <input type="password" inputmode="numeric" maxlength="1" class="pin-input pin-auth-box" style="width:42px;height:50px;text-align:center;font-size:22px; border:2px solid var(--border-color,rgba(217,164,65,0.14));border-radius:8px; background:rgba(217,164,65,0.06);color:var(--gold,#D9A441);"> <input type="password" inputmode="numeric" maxlength="1" class="pin-input pin-auth-box" style="width:42px;height:50px;text-align:center;font-size:22px; border:2px solid var(--border-color,rgba(217,164,65,0.14));border-radius:8px; background:rgba(217,164,65,0.06);color:var(--gold,#D9A441);"> <input type="password" inputmode="numeric" maxlength="1" class="pin-input pin-auth-box" style="width:42px;height:50px;text-align:center;font-size:22px; border:2px solid var(--border-color,rgba(217,164,65,0.14));border-radius:8px; background:rgba(217,164,65,0.06);color:var(--gold,#D9A441);"> <input type="password" inputmode="numeric" maxlength="1" class="pin-input pin-auth-box" style="width:42px;height:50px;text-align:center;font-size:22px; border:2px solid var(--border-color,rgba(217,164,65,0.14));border-radius:8px; background:rgba(217,164,65,0.06);color:var(--gold,#D9A441);"> <input type="password" inputmode="numeric" maxlength="1" class="pin-input pin-auth-box" style="width:42px;height:50px;text-align:center;font-size:22px; border:2px solid var(--border-color,rgba(217,164,65,0.14));border-radius:8px; background:rgba(217,164,65,0.06);color:var(--gold,#D9A441);"> </div> <button id="pinAuthCancel" class="btn btn-secondary" style="width:100%;">Cancel</button> </div> </div> <!-- Real secp256k1 address derivation. Loaded as a module before the main script so window.MoonBiteAddress is in place by the time onboarding needs it. The user's seed never leaves this page. --> <script type="module" nonce="{{ csp_nonce }}"> import { deriveFromSeedPhrase, normalizeSeedPhrase, pubkeyHashFromAddress, isValidAddress } from '{{ url_for('static', filename='moonbite-address.js') }}?v=20261016e'; import { buildSignedTransaction } from '{{ url_for('static', filename='moonbite-tx.js') }}?v=20261016e'; import { encryptSeed, decryptSeed, readSeed, isNewFormat } from '{{ url_for('static', filename='moonbite-crypto.js') }}?v=20260904a'; window.MoonBiteAddress = { deriveFromSeedPhrase, normalizeSeedPhrase, pubkeyHashFromAddress, isValidAddress }; window.MoonBiteTx = { buildSignedTransaction }; window.MoonBiteCrypto = { encryptSeed, decryptSeed, readSeed, isNewFormat }; /* PIN guard (audit M8): persisted exponential backoff after 3 wrong PINs, wipe of the encrypted seed after 10. Shared with the flagship wallet. On wipe the device returns to seed-first onboarding. */ const { createPinGuard } = await import('{{ url_for('static', filename='moonbite-pin-guard.js') }}?v=20261009a'); window.MoonBitePinGuard = createPinGuard({ storageKey: 'moonbite_pin_guard', wipeKeys: [ 'moonbite_seed_encrypted', 'moonbite_seed_encrypted_backup', 'moonbite_seed_backup_ts', 'moonbite_pin', 'moonbite_onboarding_complete', 'moonbite_wallet_setup', 'moonbite_seed_migrated', 'moonbite_seed_exposure_ack', 'moonbite_legacy_weak_seed' ], onWipe: () => { try { const uid = localStorage.getItem('moonbite_user_id'); if (uid) localStorage.removeItem('moonbite_wallet_' + uid); localStorage.removeItem('moonbite_user_id'); } catch (e) {} } }); const { generatePhrase, wordlistSize, validateMnemonic } = await import('{{ url_for('static', filename='moonbite-phrase.js') }}?v=20260904a'); window.MoonBitePhrase = { generatePhrase, wordlistSize, validateMnemonic }; window.MoonBiteReady = true; window.dispatchEvent(new Event('moonbite-address-ready')); window.dispatchEvent(new Event('moonbite-ready')); </script> <script nonce="{{ csp_nonce }}"> /* Onboarding scratch state (audit L2). The plaintext phrase and the first PIN entry used to sit in sessionStorage / window.tempPin, both readable by anything else on the page and (for sessionStorage) persisted to disk by some browsers. They now live only in these closure variables and are cleared on completion and on pagehide. */ let _pendingSeed = null; // phrase awaiting PIN encryption (create/import) let _recoverySeed = null; // phrase awaiting new PIN (forgot-PIN recovery) let _tempPin = null; // first PIN entry awaiting confirmation function clearOnboardingSecrets() { _pendingSeed = null; _recoverySeed = null; _tempPin = null; try { sessionStorage.removeItem('pendingSeedPhrase'); } catch (e) {} try { sessionStorage.removeItem('recoverySeedPhrase'); } catch (e) {} } window.addEventListener('pagehide', clearOnboardingSecrets); clearOnboardingSecrets(); // also scrub anything an older build left behind /* PIN guard access (audit M8). The guard is created by the module script above; until it exists we fall back to a permissive stub, because without the modules the decrypt cannot run either. */ function pinGuard() { return window.MoonBitePinGuard || { check: () => ({ allowed: true, waitMs: 0, remaining: 10 }), fail: () => ({ wiped: false, waitMs: 0, remaining: 10 }), reset: () => {}, message: () => 'Incorrect PIN.' }; } /* Returns a message if PIN entry is currently blocked, else null. */ function pinGuardBlockedMessage() { const st = pinGuard().check(); return st.allowed ? null : pinGuard().message(st); } /* Record a wrong PIN. Returns the message to show; if the wallet was wiped, tells the user and reloads into seed-first onboarding. */ function pinGuardFail() { const r = pinGuard().fail(); const msg = pinGuard().message(r); if (r.wiped) { alert(msg); location.reload(); } return msg; } // Service Worker registration if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/wallet-sw.js').catch(() => {}); } let deferredPrompt; let currentScreen = 'seedPhraseScreen'; // ============================================ // CUSTODIAL WALLET SYSTEM - INDIVIDUAL WALLETS // ============================================ /* --------------------------------------------------------------- Money is stored and moved as an INTEGER number of cents, never as a float. 0.1 + 0.2 !== 0.3 in IEEE-754, and a balance that drifts by a cent is a balance a miner cannot reconcile against the chain. Coins exist only at the display boundary. Doc 18: never truncate an amount, never round a balance down. Doc 11: 8 decimal places in technical contexts. --------------------------------------------------------------- */ const CENTS_PER_COIN = 100000000; /* Parse a user- or storage-supplied value into integer cents without going through float arithmetic on the fractional part. */ function toCents(v) { if (typeof v === 'number' && Number.isSafeInteger(v) && Math.abs(v) >= CENTS_PER_COIN) { return v; // already cents } const str = String(v == null ? 0 : v).trim(); const m = /^(-?)(\d*)(?:\.(\d*))?$/.exec(str); if (!m) return 0; const sign = m[1] === '-' ? -1 : 1; const whole = m[2] || '0'; const frac = (m[3] || '').slice(0, 8).padEnd(8, '0'); return sign * (Number(whole) * CENTS_PER_COIN + Number(frac)); } /* Render integer cents as MBITE. Exact: no rounding, no truncation. */ function mbite(cents) { const c = Math.trunc(Number(cents) || 0); const neg = c < 0 ? '-' : ''; const abs = Math.abs(c); const whole = Math.floor(abs / CENTS_PER_COIN); const frac = String(abs % CENTS_PER_COIN).padStart(8, '0'); return neg + whole.toLocaleString('en-US') + '.' + frac; } /* Doc 18: an irreversible action is gated on typed confirmation, not a tap or a checkbox. The sender must retype the exact amount, compared in integer cents so 1.50 and 1.5 both pass while 15 does not. */ (function wireSendConfirmation() { function bind() { const input = document.getElementById('sendConfirmInput'); const btn = document.getElementById('sendConfirmBtn'); const target = document.getElementById('sendConfirmTarget'); const hint = document.getElementById('sendConfirmHint'); if (!input || !btn || !target) return; function check() { const want = toCents(target.textContent); const got = toCents(input.value); const ok = input.value.trim() !== '' && got === want; btn.disabled = !ok; btn.style.opacity = ok ? '1' : '.45'; btn.style.cursor = ok ? 'pointer' : 'not-allowed'; btn.style.background = ok ? 'var(--success)' : 'var(--danger)'; if (hint) { hint.style.color = ok ? 'var(--success)' : (input.value.trim() ? 'var(--danger)' : 'var(--text-tertiary)'); } input.setAttribute('aria-invalid', ok ? 'false' : 'true'); } input.addEventListener('input', check); btn.addEventListener('click', function (e) { if (btn.disabled) { e.preventDefault(); e.stopPropagation(); } }); check(); } document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', bind) : bind(); })(); const CustodialWallet = { // Generate unique wallet for each user generateWallet: function(userId) { const userWalletKey = 'moonbite_wallet_' + userId; let userWallet = localStorage.getItem(userWalletKey); if (!userWallet) { // Generate new wallet const walletData = { userId: userId, /* Filled in by deriveAddressFromSeed() once the user supplies their seed. The previous build synthesized one from the userId here, which produced a string the chain rejected — and the same string for every user, so everyone shared one wallet. */ address: null, publicKey: null, pubkeyHash: null, seedPhrase: this.generateSeedPhrase(userId), /* A new wallet owns nothing until the chain says it does. Seeding a balance here showed every user 10 MBITE they could never spend. */ balanceCents: 0, currency: 'MBITE', walletName: 'Primary Wallet', blockchainHeight: 0, status: 'Active', createdAt: new Date().toISOString(), transactions: [] }; localStorage.setItem(userWalletKey, JSON.stringify(walletData)); return walletData; } const parsed = JSON.parse(userWallet); /* Migrate wallets written before balances moved to integer cents. Done once, in place, so no reconciliation is needed later against a value that was already lossy. */ if (parsed.balanceCents === undefined) { parsed.balanceCents = toCents(parsed.balance); delete parsed.balance; delete parsed.usdValue; localStorage.setItem(userWalletKey, JSON.stringify(parsed)); } return parsed; }, // Generate unique address using user ID /* Derive this wallet's real chain address from the user's seed. Runs entirely in the browser — the seed is never sent anywhere. The address is stored once, at onboarding or unlock, because deriving it needs the plaintext seed and the rest of the app reads the wallet synchronously. */ deriveAddressFromSeed: async function(seedPhrase) { if (!window.MoonBiteAddress) { console.error('address derivation module not loaded'); return null; } try { const derived = await window.MoonBiteAddress.deriveFromSeedPhrase(seedPhrase); this.updateWallet({ address: derived.address, publicKey: derived.public_key, pubkeyHash: derived.pubkey_hash }); return derived.address; } catch (e) { console.error('could not derive address:', e); return null; } }, /* Older installs hold the placeholder address that the previous build handed to every user identically. Replace it the moment we have the seed in hand. */ ensureRealAddress: async function(seedPhrase) { const wallet = this.getWallet(); if (wallet && wallet.address && wallet.address.startsWith('moon1') && wallet.pubkeyHash) { return wallet.address; } return this.deriveAddressFromSeed(seedPhrase); }, // User-provided seed phrase (no auto-generation) generateSeedPhrase: function(userId) { return ''; // User must provide their own seed }, // Simple hash function for generating unique values simpleHash: function(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(16).padStart(64, '0'); }, // Encrypt seed phrase with PIN /* Encrypt the seed under the PIN. The previous implementation XORed against simpleHash(pin), whose 56 leading zero characters meant the keystream was the literal '0' for any seed shorter than the padding — every PIN decrypted, and so did no PIN at all. Real key derivation and authenticated encryption now live in moonbite-crypto.js. */ /* The crypto module is a deferred ES module that async-imports the vendor file, so on a fresh load it can still be resolving when a returning user types their PIN. Waiting for it here is what keeps unlock from throwing on window.MoonBiteCrypto being undefined and leaving the PIN screen frozen with no feedback. */ _whenCryptoReady: async function(timeoutMs) { const deadline = Date.now() + (timeoutMs || 5000); while (!window.MoonBiteCrypto || !window.MoonBiteCrypto.readSeed) { if (Date.now() > deadline) { throw new Error('encryption module failed to load'); } await new Promise(r => setTimeout(r, 50)); } }, encryptSeedWithPIN: async function(seedPhrase, pin) { await this._whenCryptoReady(); return window.MoonBiteCrypto.encryptSeed(seedPhrase, pin); }, /* Returns the seed, or null when the PIN is wrong. AES-GCM authenticates, so a wrong PIN fails to decrypt rather than returning garbage. That is what lets the wallet check a PIN without storing it. */ decryptSeedWithPIN: async function(stored, pin) { if (!stored) return null; await this._whenCryptoReady(); const { seed, migrated } = await window.MoonBiteCrypto.readSeed(stored, pin); if (seed && migrated) { /* Written by the broken scheme. Re-encrypt it properly now that the plaintext is in hand — but note the old blob was readable by anyone with the device, so this seed should be considered already exposed. */ try { localStorage.setItem('moonbite_seed_encrypted', await window.MoonBiteCrypto.encryptSeed(seed, pin)); localStorage.setItem('moonbite_seed_migrated', '1'); } catch (e) { console.error('could not migrate stored seed:', e); } } return seed; }, /* Check a PIN by trying to decrypt with it. The PIN used to be compared against a copy kept in localStorage in the clear, sitting beside the seed it was supposed to protect. Nothing is stored to compare against now. */ verifyPIN: async function(pin) { const stored = localStorage.getItem('moonbite_seed_encrypted'); if (!stored) return false; if (!window.MoonBiteCrypto.isNewFormat(stored)) { /* A legacy blob carries no way to check a PIN — the old scheme ignored it. Accept, migrate, and from then on the PIN is real. */ return true; } return (await window.MoonBiteCrypto.decryptSeed(stored, pin)) !== null; }, // Get user's wallet getWallet: function() { const userId = this.getUserId(); return this.generateWallet(userId); }, // Get or create user ID getUserId: function() { let userId = localStorage.getItem('moonbite_user_id'); if (!userId) { userId = 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); localStorage.setItem('moonbite_user_id', userId); } return userId; }, // Update wallet updateWallet: function(updates) { const userId = this.getUserId(); const userWalletKey = 'moonbite_wallet_' + userId; const wallet = this.getWallet(); const updated = { ...wallet, ...updates }; localStorage.setItem(userWalletKey, JSON.stringify(updated)); this.syncAllScreens(); return updated; }, /* Ask the chain what this address actually holds. The balance in localStorage is a cache for instant paint, never the source of truth — only the UTXO set is. A wallet that believes its own cache will happily show coins the chain never credited. Failures leave the cached value alone rather than blanking the screen, so a dropped request reads as "stale", not "your funds are gone". */ refreshBalance: async function() { const wallet = this.getWallet(); // No address until the user's seed has been supplied; there is // nothing to ask the chain about yet. if (!wallet || !wallet.address) return null; try { const res = await fetch( '/api/chain/address/' + encodeURIComponent(wallet.address), { headers: { 'Accept': 'application/json' } } ); if (!res.ok) return null; const data = await res.json(); if (data.status !== 'success') return null; // balance_units is integer cents from the server; keep it // integral so no float ever touches a balance. const cents = Number(data.balance_units); if (!Number.isSafeInteger(cents)) return null; // Mining rewards still inside the 100-block maturity window. // Display only: sends compare against balance_units alone. const maturing = Number(data.immature_units) || 0; const firstMature = (data.immature_utxos || []).reduce(function(m, u) { const h = Number(u.matures_at_height); return Number.isSafeInteger(h) && (m === null || h < m) ? h : m; }, null); /* A net increase since we last looked is incoming MBITE (a receipt or a mining reward). A send never raises the balance — its change returns inside the same tx that reduces it — so this does not double-count our own sends. Seed the baseline silently on the first read so a pre-existing balance is not logged as a fresh receipt. */ const prev = wallet.lastSeenBalanceCents; if (typeof prev === 'number' && cents > prev) { try { logActivity({ type: 'receive', amountUnits: cents - prev, status: 'confirmed' }); } catch (e) { /* activity is best-effort */ } } if (cents !== wallet.balanceCents || data.height !== wallet.blockchainHeight || prev !== cents || maturing !== wallet.maturingCents || firstMature !== wallet.firstMatureHeight) { this.updateWallet({ balanceCents: cents, maturingCents: maturing, firstMatureHeight: firstMature, blockchainHeight: data.height, lastSeenBalanceCents: cents }); } return cents; } catch (e) { return null; } }, // Sync wallet data to all screens syncAllScreens: function() { const wallet = this.getWallet(); // Surface the exposed-seed notice on a migrated wallet, unless // the holder has already acknowledged it. const warn = document.getElementById('seedRotationWarning'); if (warn) { const exposed = localStorage.getItem('moonbite_seed_migrated') === '1' && localStorage.getItem('moonbite_seed_exposure_ack') !== '1'; warn.style.display = exposed ? 'block' : 'none'; } // Update Balance Card (Dashboard) const balanceAmount = document.querySelector('.balance-amount'); if (balanceAmount) { // Write only the number node: assigning textContent here would // delete the nested .balance-unit span and lose the ticker. balanceAmount.firstChild ? balanceAmount.firstChild.nodeValue = mbite(wallet.balanceCents) : balanceAmount.prepend(mbite(wallet.balanceCents)); } // Update Receive Screen Address const receiveAddress = document.getElementById('receiveAddress'); if (receiveAddress) { // Reads 'Unlock…' rather than 'null' before derivation. receiveAddress.textContent = wallet.address || 'Unlock your wallet to reveal your address'; } // Update Blockchain Height const blockchainHeightEl = document.getElementById('blockchainHeight'); if (blockchainHeightEl) { blockchainHeightEl.textContent = wallet.blockchainHeight; } // Update Wallet Balance Display const walletBalanceEl = document.getElementById('walletBalance'); if (walletBalanceEl) { walletBalanceEl.textContent = mbite(wallet.balanceCents) + ' MBITE'; } // Update all wallet items on Wallets Screen const walletItems = document.querySelectorAll('.wallet-item'); walletItems.forEach((item, index) => { const balanceEl = item.querySelector('.wallet-balance'); if (balanceEl && index === 0) { // Primary wallet balanceEl.textContent = mbite(wallet.balanceCents) + ' MBITE'; } }); // Update wallet name display const walletNameDisplay = document.querySelector('.wallet-name-display'); if (walletNameDisplay) { walletNameDisplay.textContent = wallet.walletName; } // Update wallet amount const walletAmount = document.querySelector('.wallet-amount'); if (walletAmount) { walletAmount.textContent = mbite(wallet.balanceCents) + ' MBITE'; } // Update premium wallet card const premiumWalletCard = document.querySelector('.premium-wallet-card'); if (premiumWalletCard) { const nameEl = premiumWalletCard.querySelector('.wallet-name-display'); if (nameEl) nameEl.textContent = wallet.walletName; const amountEl = premiumWalletCard.querySelector('.wallet-amount'); if (amountEl) amountEl.textContent = mbite(wallet.balanceCents) + ' MBITE'; } // TOGGLE EMPTY VS BALANCE STATE const emptyStateCard = document.getElementById('emptyStateCard'); const balanceStateCard = document.getElementById('balanceStateCard'); const maturingCents = wallet.maturingCents || 0; const isZeroBalance = wallet.balanceCents === 0 && maturingCents === 0; // Freshly mined coins: show them as maturing, not as nothing. const vaultSub = document.getElementById('vaultSub'); if (vaultSub) { if (maturingCents > 0) { let line = '+ ' + mbite(maturingCents) + ' MBITE maturing'; if (wallet.firstMatureHeight != null) { const left = wallet.firstMatureHeight - (wallet.blockchainHeight || 0); line += left > 0 ? ' · first unlocks at block #' + wallet.firstMatureHeight + ' (' + left + ' block' + (left === 1 ? '' : 's') + ' to go)' : ' · unlocking now'; } vaultSub.textContent = line; } else { vaultSub.textContent = 'Earned through mining'; } } const balanceLabel = document.querySelector('#balanceStateCard .balance-label'); if (balanceLabel) balanceLabel.textContent = maturingCents > 0 ? 'Spendable Balance' : 'Total Balance'; if (emptyStateCard && balanceStateCard) { if (isZeroBalance) { emptyStateCard.style.display = 'block'; balanceStateCard.style.display = 'none'; // Populate empty state address const emptyAddrDisplay = document.getElementById('emptyAddrDisplay'); if (emptyAddrDisplay) { emptyAddrDisplay.textContent = wallet.address || 'Unlock your wallet to reveal your address'; } } else { emptyStateCard.style.display = 'none'; balanceStateCard.style.display = 'block'; // Update balance state values const balanceSpan = document.getElementById('balanceAmount'); if (balanceSpan) balanceSpan.textContent = mbite(wallet.balanceCents); const walletNameSpan = document.getElementById('walletNameDisplay'); if (walletNameSpan) walletNameSpan.textContent = wallet.walletName; const walletAmtSpan = document.getElementById('walletAmount'); if (walletAmtSpan) walletAmtSpan.textContent = mbite(wallet.balanceCents) + ' MBITE'; } } } }; // Initialize - show PIN entry for returning users, or PIN setup for new users /* Delegated event dispatch. Every control used to carry an inline onclick/onkeyup attribute, which forced script-src 'unsafe-inline' — and that also permits an injected script tag to run, which is the actual XSS risk. Handlers are now data-attributes routed through the whitelist below, so the page runs under a nonce-based CSP with no unsafe-inline. Dispatch is by name against an explicit allow-list: nothing here evaluates a string, so a crafted data-fn cannot call anything the page did not intend to expose. */ const MB_ACTIONS = { // zero-arg and single-string-arg functions reachable from markup saveSetupPINAndContinue: () => saveSetupPINAndContinue(), saveRecoveredPIN: () => saveRecoveredPIN(), verifySeedPhrase: () => verifySeedPhrase(), verifyRecoverySeed: () => verifyRecoverySeed(), verifyPINAndLogin: () => verifyPINAndLogin(), tryBiometricLogin: () => tryBiometricLogin(), showPINFallback: () => showPINFallback(), toggleTheme: () => toggleTheme(), copyAddress: () => copyAddress(), copyGeneratedPhrase: () => copyGeneratedPhrase(), confirmGeneratedPhrase: () => confirmGeneratedPhrase(), checkPhraseBackup: () => checkPhraseBackup(), createNewWallet: () => createNewWallet(), revealRecoveryPhrase: () => revealRecoveryPhrase(), hideRevealedPhrase: () => hideRevealedPhrase(), acknowledgeSeedExposure: () => acknowledgeSeedExposure(), beginSeedRotation: () => beginSeedRotation(), sendTransaction: () => sendTransaction(), setMaxSendAmount: () => setMaxSendAmount(), saveContact: () => saveContact(), completeOnboarding: () => completeOnboarding(), updatePasswordStrength: () => updatePasswordStrength(), handlePinFallbackComplete: () => handlePinFallbackComplete(), handlePinNext: () => handlePinNext(), handlePinConfirmComplete: () => handlePinConfirmComplete(), handleNewPinComplete: () => handleNewPinComplete(), proceedToConfirmPin: () => proceedToConfirmPin(), backToCreatePin: () => backToCreatePin(), copyToClipboard: (arg) => copyToClipboard(arg), togglePasswordVisibility: (arg) => togglePasswordVisibility(arg), generateQRCode: () => generateQRCode(), reviewAndSend: () => reviewAndSend(), shareWalletAddress: () => shareWalletAddress(), }; function mbDispatch(el, evt) { const act = el.getAttribute('data-act'); const arg = el.getAttribute('data-arg'); switch (act) { case 'screen': showScreen(arg); break; case 'screen-nav': showScreen(arg); setActiveNav(el); break; case 'pinfocus': movePinFocus(el, arg); break; case 'contactsearch': renderContacts(el.value); break; case 'feespeed': setFeeSpeed(el, arg); break; case 'setfee': { const f = document.getElementById('customFeeInput'); if (f) f.value = arg; break; } case 'gate': { const btn = document.getElementById(arg); if (btn) btn.disabled = !el.checked; break; } case 'copyel': { const src = document.getElementById(arg); if (src) copyToClipboard(src.textContent); break; } case 'call': { const fn = MB_ACTIONS[el.getAttribute('data-fn')]; if (typeof fn === 'function') { arg === null ? fn() : fn(arg); } else { console.warn('unmapped action:', el.getAttribute('data-fn')); } break; } default: break; } } // One listener per event type, on the document — works for elements // added later (contacts, activity, wallets) with no re-binding. ['click', 'keyup', 'change', 'input'].forEach(type => { document.addEventListener(type, (e) => { const el = e.target.closest ? e.target.closest('[data-act]') : null; if (!el) return; const act = el.getAttribute('data-act'); // Route each action to the event it was written for. const wants = act === 'pinfocus' ? 'keyup' : act === 'contactsearch' ? 'input' : act === 'gate' ? 'change' : (act === 'call' && el.getAttribute('data-fn') === 'updatePasswordStrength') ? 'change' : (act === 'call' && ['handlePinFallbackComplete', 'handlePinNext', 'handlePinConfirmComplete', 'handleNewPinComplete'] .includes(el.getAttribute('data-fn'))) ? 'keyup' : 'click'; if (type !== wants) return; mbDispatch(el, e); }); }); function initializeScreens() { // Initialize user's custodial wallet const userWallet = CustodialWallet.getWallet(); // Remove active from all screens first document.querySelectorAll('.screen').forEach(screen => { screen.classList.remove('active'); }); // Check if onboarding is complete const onboardingComplete = localStorage.getItem('moonbite_onboarding_complete'); if (onboardingComplete) { // Returning user - show PIN entry/verification screen const pinVerifyScreen = document.getElementById('pinVerifyScreen'); if (pinVerifyScreen) { pinVerifyScreen.classList.add('active'); currentScreen = 'pinVerifyScreen'; setTimeout(() => { const first = document.getElementById('verifyPin0'); if (first) first.focus(); }, 250); } } else { /* First-time user: show the setup fork so they choose between creating a new wallet and importing an existing phrase, rather than being dropped straight onto seed entry with no way to get a phrase if they do not have one. */ const setup = document.getElementById('walletSetupScreen'); if (setup) { setup.classList.add('active'); currentScreen = 'walletSetupScreen'; } } initTheme(); // Sync user's wallet data to all screens setTimeout(() => { CustodialWallet.syncAllScreens(); // Paint the cached value first, then reconcile against the // chain; updateWallet() re-syncs the screens if it differs. CustodialWallet.refreshBalance(); }, 100); // Keep the balance current while the wallet is open: once a minute // (blocks land about every 2 minutes). if (!window._mbBalancePoll) { window._mbBalancePoll = setInterval(() => { if (!document.hidden) CustodialWallet.refreshBalance(); }, 60000); } } // Call initialization immediately and also on load as fallback if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeScreens); } else { initializeScreens(); } window.addEventListener('load', initializeScreens); // PWA Install prompt window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; document.getElementById('installPrompt').classList.add('show'); }); document.getElementById('installBtn')?.addEventListener('click', async () => { if (deferredPrompt) { deferredPrompt.prompt(); deferredPrompt = null; document.getElementById('installPrompt').classList.remove('show'); } }); document.getElementById('dismissBtn')?.addEventListener('click', () => { document.getElementById('installPrompt').classList.remove('show'); }); // Screen navigation function copyToClipboard(text) { if (navigator.clipboard) { navigator.clipboard.writeText(text).then(() => { const btn = event.target; const originalText = btn.textContent; btn.textContent = 'Copied!'; btn.style.background = 'var(--success)'; setTimeout(() => { btn.textContent = originalText; btn.style.background = ''; }, 2000); }).catch(() => { alert('Could not copy address. Try manually selecting and copying.'); }); } else { alert('Clipboard not available. Try manually selecting and copying.'); } } function showScreen(screenId) { // Sync user's wallet data before changing screens CustodialWallet.syncAllScreens(); // Update confirmation screen with actual user input if (screenId === 'confirmSendScreen') { const sc = document.getElementById('sendConfirmInput'); if (sc) { sc.value = ''; sc.dispatchEvent(new Event('input')); } const recipient = document.getElementById('sendRecipient')?.value || '-'; const amount = document.getElementById('sendAmount')?.value || '0'; document.getElementById('confirmRecipient').textContent = recipient; document.querySelector('[id="totalAmountDisplay"]')?.parentElement?.querySelector('.card-value')?.textContent?.replace(/\d+\.?\d*/, amount) || null; } // Remove active from ALL screens first document.querySelectorAll('.screen').forEach(screen => { screen.classList.remove('active'); }); // Show the bottom nav only on signed-in app screens, never during // seed/PIN onboarding or recovery. const APP_SCREENS = new Set([ 'dashboardScreen', 'walletsScreen', 'contactsScreen', 'securityScreen', 'sendScreen', 'receiveScreen', 'confirmSendScreen', 'transactionSuccessScreen', 'qrScannerScreen', 'addContactScreen', 'editNetworkFeeScreen', 'settingsScreen', 'activityScreen', 'revealPhraseScreen' ]); const nav = document.querySelector('.bottom-nav'); if (nav) nav.classList.toggle('show', APP_SCREENS.has(screenId)); // Show only the new screen const newScreenEl = document.getElementById(screenId); if (newScreenEl) { newScreenEl.classList.add('active'); currentScreen = screenId; updateTopBar(screenId); // Generate QR code for receive screen if (screenId === 'receiveScreen') { setTimeout(generateQRCode, 100); } // Populate wallets screen with user's actual wallet data if (screenId === 'walletsScreen') { setTimeout(populateWalletsScreen, 100); } // Render the user's own (initially empty) contact list. if (screenId === 'contactsScreen') { setTimeout(() => renderContacts(), 50); } // Render activity and refresh pending confirmations. if (screenId === 'activityScreen') { setTimeout(() => { renderActivity(); refreshActivityConfirmations(); }, 50); } // Show spendable balance on the Send screen (refreshed first). if (screenId === 'sendScreen') { updateSendAvailable(); CustodialWallet.refreshBalance().then(updateSendAvailable); } } } function updateTopBar(screenId) { const titles = { 'dashboardScreen': 'MoonBite', 'walletsScreen': 'My Wallet', 'sendScreen': 'Send MBITE', 'receiveScreen': 'Receive MBITE', 'securityScreen': 'Security', 'contactsScreen': 'Contacts', 'createPasswordScreen': 'Create Password', 'seedPhraseScreen': 'Recovery Phrase', 'successScreen': 'Success', 'createWalletScreen': 'Create Wallet', 'addContactScreen': 'Add Contact', 'confirmSendScreen': 'Confirm Send' }; const screenTitle = document.getElementById('screenTitle'); if (screenTitle) { screenTitle.textContent = titles[screenId] || 'MoonBite'; } updateBackButton(screenId); } function updateBackButton(screenId) { const backBtn = document.getElementById('backBtn'); const homeScreens = ['dashboardScreen', 'walletsScreen', 'contactsScreen', 'securityScreen']; if (homeScreens.includes(screenId)) { backBtn.classList.add('hidden'); } else { backBtn.classList.remove('hidden'); } } document.getElementById('backBtn')?.addEventListener('click', () => { const previousScreens = { 'createWalletScreen': 'walletsScreen', 'addContactScreen': 'contactsScreen', 'createPasswordScreen': 'securityScreen', 'seedPhraseScreen': 'createPasswordScreen', 'confirmSendScreen': 'sendScreen', 'successScreen': 'dashboardScreen' }; showScreen(previousScreens[currentScreen] || 'dashboardScreen'); }); function setActiveNav(el) { document.querySelectorAll('.nav-item').forEach(item => item.classList.remove('active')); el.classList.add('active'); } // Single source of truth for the receive address: the wallet's OWN // derived, checksum-valid address. Never a hardcoded placeholder -- a // placeholder address is not spendable by the user, so any MBITE sent to // a copied/shared placeholder is permanently lost. function _realWalletAddress() { const w = (typeof CustodialWallet !== 'undefined') && CustodialWallet.getWallet(); const addr = w && w.address; if (addr && window.MoonBiteAddress && window.MoonBiteAddress.isValidAddress(addr)) { return addr; } return null; } function copyAddress() { const address = _realWalletAddress(); if (!address) { alert('Unlock your wallet first to reveal your receive address.'); return; } navigator.clipboard.writeText(address); // Show visual feedback const feedback = document.getElementById('copyFeedback'); if (feedback) { feedback.style.display = 'block'; setTimeout(() => { feedback.style.display = 'none'; }, 2000); } } function shareWalletAddress() { const address = _realWalletAddress(); if (!address) { alert('Unlock your wallet first to reveal your receive address.'); return; } if (navigator.share) { navigator.share({ title: 'MoonBite Mining Address', text: 'Send your mining rewards to this address: ' + address, url: window.location.href }); } else { copyAddress(); alert('Share address: ' + address); } } function generateQRCode() { const container = document.getElementById('qrCodeContainer'); if (!container) return; // Clear previous QR code container.innerHTML = ''; // Only ever encode the wallet's own valid address. Encoding a // placeholder would produce a QR that sends funds nowhere spendable. const address = _realWalletAddress(); if (!address) { container.innerHTML = '<div style="color: #888; font-size: 12px; padding: 20px; text-align: center;">Unlock your wallet to show its QR code</div>'; return; } // Generate inline SVG QR code (no external library needed) try { const qrCanvas = generateQRCodeSVG(address, 160); container.appendChild(qrCanvas); } catch (error) { console.error('QR Code generation error:', error); // Fallback: show the address as text if QR fails container.innerHTML = '<div style="color: #888; font-size: 12px; padding: 20px; text-align: center;">QR Code unavailable<br>Use address below</div>'; } } function generateQRCodeSVG(text, size) { // Simple QR code generator using canvas const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d'); // Fill background ctx.fillStyle = '#161A22'; ctx.fillRect(0, 0, size, size); // Draw a simple pattern (simplified QR representation) // This creates a scannable pattern for the address ctx.fillStyle = '#FFFFFF'; const moduleCount = 21; // Simple 21x21 grid const moduleSize = size / moduleCount; // Top-left position marker for (let i = 0; i < 7; i++) { for (let j = 0; j < 7; j++) { if ((i === 0 || i === 6 || j === 0 || j === 6) || (i >= 2 && i <= 4 && j >= 2 && j <= 4)) { ctx.fillRect(i * moduleSize, j * moduleSize, moduleSize - 0.5, moduleSize - 0.5); } } } // Top-right position marker for (let i = 0; i < 7; i++) { for (let j = 0; j < 7; j++) { if ((i === 0 || i === 6 || j === 0 || j === 6) || (i >= 2 && i <= 4 && j >= 2 && j <= 4)) { ctx.fillRect((moduleCount - 7 + i) * moduleSize, j * moduleSize, moduleSize - 0.5, moduleSize - 0.5); } } } // Bottom-left position marker for (let i = 0; i < 7; i++) { for (let j = 0; j < 7; j++) { if ((i === 0 || i === 6 || j === 0 || j === 6) || (i >= 2 && i <= 4 && j >= 2 && j <= 4)) { ctx.fillRect(i * moduleSize, (moduleCount - 7 + j) * moduleSize, moduleSize - 0.5, moduleSize - 0.5); } } } // Data area (hash-based pattern for the address) const hash = text.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); for (let i = 0; i < moduleCount; i++) { for (let j = 0; j < moduleCount; j++) { if (i < 9 || i > 11 || j > 8) { // Avoid timing patterns if (((hash * (i + 1) * (j + 1)) % 256) > 128) { ctx.fillRect(i * moduleSize, j * moduleSize, moduleSize - 0.5, moduleSize - 0.5); } } } } return canvas; } // Toggle switches document.querySelectorAll('.toggle-switch').forEach(toggle => { toggle.addEventListener('click', function() { this.classList.toggle('active'); }); }); // Settings button document.getElementById('settingsBtn')?.addEventListener('click', () => { showScreen('securityScreen'); setActiveNav(document.querySelectorAll('.nav-item')[3]); }); // Notifications button document.getElementById('notificationBtn')?.addEventListener('click', () => { alert('No new notifications'); }); // ============================================================ // Onboarding Functions // ============================================================ function skipToWalletSetup() { showScreen('walletSetupScreen'); } function movePinFocus(input, nextId) { if (input.value.length === 1 && nextId !== 'confirmPin') { document.getElementById(nextId).focus(); } } /* In-page replacement for prompt('Enter your PIN...'). Native prompt() is not reliably available - most importantly in a PWA launched from the home screen (this app's own manifest sets "display": "standalone"), which is exactly how users are pushed to install it - so a feature gated behind prompt() could silently stop working for them with no way to proceed. This resolves with the 6-digit string, or null if the user cancels; callers already treat a falsy result as "cancelled", the same contract prompt() had. */ let _pinAuthResolve = null; function _pinAuthBoxes() { return Array.from(document.querySelectorAll('#pinAuthOverlay .pin-auth-box')); } function _pinAuthClose(result) { const overlay = document.getElementById('pinAuthOverlay'); overlay.style.display = 'none'; document.removeEventListener('keydown', _pinAuthEscHandler); _pinAuthBoxes().forEach(b => { b.value = ''; }); document.getElementById('pinAuthError').textContent = ''; const resolve = _pinAuthResolve; _pinAuthResolve = null; if (resolve) resolve(result); } function _pinAuthEscHandler(e) { if (e.key === 'Escape') _pinAuthClose(null); } function requestPinAsync(title, errorText) { return new Promise((resolve) => { _pinAuthResolve = resolve; document.getElementById('pinAuthTitle').textContent = title || 'Enter your PIN'; document.getElementById('pinAuthError').textContent = errorText || ''; document.getElementById('pinAuthOverlay').style.display = 'flex'; document.addEventListener('keydown', _pinAuthEscHandler); const boxes = _pinAuthBoxes(); boxes.forEach(b => { b.value = ''; }); boxes[0].focus(); }); } (function initPinAuthOverlay() { const boxes = _pinAuthBoxes(); boxes.forEach((box, i) => { box.addEventListener('input', () => { box.value = box.value.replace(/[^0-9]/g, '').slice(0, 1); if (box.value && i < boxes.length - 1) { boxes[i + 1].focus(); return; } if (box.value && i === boxes.length - 1) { const pin = boxes.map(b => b.value).join(''); if (pin.length === 6) _pinAuthClose(pin); } }); box.addEventListener('keydown', (e) => { if (e.key === 'Backspace' && !box.value && i > 0) boxes[i - 1].focus(); }); }); const cancelBtn = document.getElementById('pinAuthCancel'); if (cancelBtn) cancelBtn.addEventListener('click', () => _pinAuthClose(null)); const overlay = document.getElementById('pinAuthOverlay'); if (overlay) overlay.addEventListener('click', (e) => { if (e.target === overlay) _pinAuthClose(null); }); })(); function handlePinComplete() { const lastDigit = document.getElementById('confirmPinDigit6'); if (lastDigit.value.length === 1) { // Auto-validate when last digit is entered validateAndSavePIN(); } } // USER-CENTRIC PIN: ONE TIME SETUP, NO CONFIRMATION function handlePinSetupComplete() { const pin = document.getElementById('pinDigit1').value + document.getElementById('pinDigit2').value + document.getElementById('pinDigit3').value + document.getElementById('pinDigit4').value + document.getElementById('pinDigit5').value + document.getElementById('pinDigit6').value; if (pin.length === 6) { // Auto-submit when all 6 digits entered saveSetupPINAndContinue(); } } function proceedToConfirmPin() { const pin = document.getElementById('pinDigit1').value + document.getElementById('pinDigit2').value + document.getElementById('pinDigit3').value + document.getElementById('pinDigit4').value + document.getElementById('pinDigit5').value + document.getElementById('pinDigit6').value; if (pin.length !== 6) { alert('Please enter a 6-digit PIN'); return; } // Hold the first PIN in a closure variable until it is confirmed _tempPin = pin; // Hide create step, show confirm step document.getElementById('pinCreateStep').style.display = 'none'; document.getElementById('pinConfirmStep').style.display = 'block'; // Focus on first confirm digit setTimeout(() => { document.getElementById('pinConfirm1').focus(); }, 100); } function backToCreatePin() { // Clear confirm inputs for (let i = 1; i <= 6; i++) { document.getElementById('pinConfirm' + i).value = ''; } // Show create step, hide confirm step document.getElementById('pinCreateStep').style.display = 'block'; document.getElementById('pinConfirmStep').style.display = 'none'; document.getElementById('pinMismatchError').style.display = 'none'; // Focus on first digit setTimeout(() => { document.getElementById('pinDigit1').focus(); }, 100); } function handlePinNext() { const pin6 = document.getElementById('pinDigit6'); if (pin6.value) { proceedToConfirmPin(); } } function handlePinConfirmComplete() { const confirmPin = document.getElementById('pinConfirm1').value + document.getElementById('pinConfirm2').value + document.getElementById('pinConfirm3').value + document.getElementById('pinConfirm4').value + document.getElementById('pinConfirm5').value + document.getElementById('pinConfirm6').value; if (confirmPin.length === 6) { if (confirmPin === _tempPin) { // PINs match - save saveSetupPINAndContinue(); } else { // PINs don't match - show error document.getElementById('pinMismatchError').style.display = 'block'; // Clear confirm inputs for (let i = 1; i <= 6; i++) { document.getElementById('pinConfirm' + i).value = ''; } setTimeout(() => { document.getElementById('pinConfirm1').focus(); }, 100); } } } async function saveSetupPINAndContinue() { const pin = _tempPin; const seedPhrase = _pendingSeed; if (!seedPhrase) { alert('Error: Seed phrase not found. Please re-enter your seed.'); showScreen('seedPhraseScreen'); return; } // Encrypt seed phrase with PIN using simple XOR + hash // In production: use proper AES-256-GCM const encryptedSeed = await CustodialWallet.encryptSeedWithPIN(seedPhrase, pin); /* The PIN itself is never stored. It is checked by decrypting, which the previous build could not do: it kept the PIN in localStorage in the clear, next to the seed. */ localStorage.removeItem('moonbite_pin'); localStorage.setItem('moonbite_seed_encrypted', encryptedSeed); localStorage.setItem('moonbite_onboarding_complete', 'true'); pinGuard().reset(); /* Derive the real chain address now, while the plaintext seed is still in hand — afterwards it exists only PIN-encrypted. Await it so the dashboard opens with the address ready. */ try { await CustodialWallet.deriveAddressFromSeed(seedPhrase); } catch (e) { /* address derives again on next refresh */ } clearOnboardingSecrets(); CustodialWallet.refreshBalance(); // Show success feedback const btn = document.querySelector('[data-act="call"][data-fn="saveSetupPINAndContinue"]'); if (btn) { btn.textContent = 'PIN Saved!'; btn.style.background = 'linear-gradient(135deg, #2E9E6B, #2E9E6B)'; } // Onboarding complete -> open the wallet (was looping back to the // 'Set Up Your Wallet' screen, which looked stuck). setTimeout(() => { showScreen('dashboardScreen'); }, 500); } // RETURNING USERS: BIOMETRIC FIRST, PIN AS FALLBACK function tryBiometricLogin() { /* Kept only because old markup may live in installed PWA caches. The previous body showed the dashboard without authenticating or decrypting anything. Now it routes to the real login. */ showPINFallback(); } function showPINFallback() { const fallbackArea = document.getElementById('pinFallbackArea'); fallbackArea.style.display = 'block'; document.getElementById('verifyPin0').focus(); } // SIMPLIFIED PIN VERIFICATION - NO CONFIRMATION NEEDED function validateAndSavePIN() { // This is kept for backward compatibility saveSetupPINAndContinue(); } function copySeedPhrase() { // Delegate to the real generated-phrase copy; never copy a fixed // phrase (the old body pasted a hardcoded seed). if (typeof copyGeneratedPhrase === 'function') return copyGeneratedPhrase(); } // Password visibility toggle function togglePasswordVisibility(fieldId) { const field = document.getElementById(fieldId); if (field) { field.type = field.type === 'password' ? 'text' : 'password'; } } // Transaction Fee Speed Handler function setFeeSpeed(button, speed) { const buttons = button.parentElement.querySelectorAll('.btn'); buttons.forEach(btn => { btn.style.background = 'rgba(26, 77, 122, 0.2)'; btn.style.color = 'var(--gold)'; }); button.style.background = 'linear-gradient(135deg, var(--fire-orange), #B8863B)'; button.style.color = 'white'; // Priced for this chain's 1 unit/vB relay floor; a plain send is ~141 vB. // The signer raises a fee that is too small for a many-coin spend. const fees = { 'FAST': '0.0001', 'STANDARD': '0.00002', 'SLOW': '0.00001' }; document.getElementById('networkFeeDisplay').textContent = fees[speed] + ' MBITE'; document.getElementById('feeSpeedDisplay').textContent = speed; const amount = parseFloat(document.getElementById('sendAmount').value) || 1.5; const fee = parseFloat(fees[speed]); document.getElementById('totalAmountDisplay').textContent = (amount + fee).toFixed(8).replace(/0+$/, '').replace(/\.$/, '') + ' MBITE'; } /* Spend: build the transaction, sign it here, broadcast the result. The server is only ever handed an already-signed transaction, so it has nothing it could spend on the user's behalf. */ async function sendTransaction() { const btn = document.getElementById('sendConfirmBtn'); const original = btn ? btn.textContent : ''; const fail = (msg) => { alert(msg); if (btn) { btn.disabled = false; btn.textContent = original; } }; try { if (!window.MoonBiteTx || !window.MoonBiteAddress) { return fail('Wallet is still loading. Please try again in a moment.'); } const recipient = (document.getElementById('sendRecipient').value || '').trim(); if (!window.MoonBiteAddress.isValidAddress(recipient)) { return fail('That recipient address is not valid. Check it and try again.'); } const wallet = CustodialWallet.getWallet(); if (!wallet || !wallet.address) { return fail('Unlock your wallet before sending.'); } if (recipient === wallet.address) { return fail('That is your own address.'); } const amountUnits = toCents(document.getElementById('sendAmount').value); if (!amountUnits || amountUnits <= 0) { return fail('Enter an amount greater than zero.'); } const feeText = (document.getElementById('networkFeeDisplay') .textContent || '0').replace(/[^0-9.]/g, ''); const feeUnits = toCents(feeText) || 0; /* The seed is only ever stored PIN-encrypted, so spending asks for the PIN. That is also the point of the PIN: it is what stands between someone holding this device and the funds. */ const blocked = pinGuardBlockedMessage(); if (blocked) return fail(blocked + ' The transaction was not sent.'); const pin = await requestPinAsync('Enter your PIN to authorize this transaction'); if (!pin) return fail('Transaction cancelled.'); // A wrong PIN simply fails to decrypt; there is no stored copy // to compare against. const seed = await CustodialWallet.decryptSeedWithPIN( localStorage.getItem('moonbite_seed_encrypted'), pin); if (!seed) return fail(pinGuardFail() + ' The transaction was not sent.'); pinGuard().reset(); if (btn) { btn.disabled = true; btn.textContent = 'Signing...'; } /* Spend against what the chain says is unspent right now, not a cached balance that may already be stale. */ const balRes = await fetch( '/api/chain/address/' + encodeURIComponent(wallet.address)); if (!balRes.ok) return fail('Could not reach the network. Try again.'); const bal = await balRes.json(); if (amountUnits + feeUnits > bal.balance_units) { return fail('Not enough MBITE for that amount plus the network fee.'); } const built = await window.MoonBiteTx.buildSignedTransaction({ seedPhrase: seed, toPubkeyHash: window.MoonBiteAddress.pubkeyHashFromAddress(recipient), amountUnits: amountUnits, feeUnits: feeUnits, utxos: bal.utxos || [] }); if (btn) btn.textContent = 'Broadcasting...'; const res = await fetch('/api/chain/broadcast', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rawtx: built.rawHex }) }); const result = await res.json(); if (!res.ok || result.status !== 'success') { return fail(result.message || 'The network rejected this transaction.'); } // Record it for history, but never let a bookkeeping failure // imply the spend itself failed - it is already on the chain. fetch('/api/wallet/transaction/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ txid: result.txid, amount_units: amountUnits, fee_units: built.fee, from_address: wallet.address, to_address: recipient, status: 'pending' }) }).catch(() => {}); const txidEl = document.getElementById('successTxid'); if (txidEl) txidEl.textContent = result.txid; const amtEl = document.getElementById('successAmount'); if (amtEl) amtEl.textContent = mbite(amountUnits); // Watch this tx and update the success screen in place when it // lands in a block, instead of leaving it on "waiting". startSuccessConfirmationPoll(result.txid); // Record it in the local activity log as pending; the Activity // screen promotes it to confirmed once it lands in a block. try { logActivity({ type: 'send', to: recipient, amountUnits: amountUnits, txid: result.txid, status: 'pending' }); } catch (e) { /* activity is best-effort */ } await CustodialWallet.refreshBalance(); if (btn) { btn.disabled = false; btn.textContent = original; } showScreen('transactionSuccessScreen'); } catch (e) { fail(e && e.message === 'insufficient funds' ? 'Not enough MBITE for that amount plus the network fee.' : 'Could not send: ' + (e && e.message ? e.message : 'unknown error')); } } /* Dismiss the exposed-seed notice without rotating. Records that the holder has seen it, so it does not nag on every unlock — the risk is theirs to accept. */ function acknowledgeSeedExposure() { localStorage.setItem('moonbite_seed_exposure_ack', '1'); const warn = document.getElementById('seedRotationWarning'); if (warn) warn.style.display = 'none'; } /* Start over with a fresh recovery phrase. Rotation on a seed-first, non-custodial wallet can only be done by the holder: generate a new phrase and move any balance to its address. This clears the exposed wallet from the device and returns to onboarding so a new phrase can be entered. It does NOT sweep funds automatically — the old phrase (which the holder generated and should still hold) is what authorises that, and doing it silently could strand a balance if the transaction failed. */ function beginSeedRotation() { const ok = confirm( "Rotate to a new wallet?\n\n" + "If this wallet holds any MBITE, send it to your NEW address " + "first — you will need your current recovery phrase to do that.\n\n" + "This removes the exposed wallet from this device and starts a " + "fresh setup. Continue?" ); if (!ok) return; const uid = localStorage.getItem('moonbite_user_id'); [ 'moonbite_seed_encrypted', 'moonbite_pin', 'moonbite_onboarding_complete', 'moonbite_seed_migrated', 'moonbite_seed_exposure_ack', 'moonbite_user_id' ].forEach(k => localStorage.removeItem(k)); if (uid) localStorage.removeItem('moonbite_wallet_' + uid); location.reload(); // reloads into seed-first onboarding } /* Contacts — the user's own address book, stored per wallet. These were three hardcoded demo people baked into the HTML, so a fresh wallet appeared to already have contacts. They are now real and start empty. */ function contactsKey() { return 'moonbite_contacts_' + (localStorage.getItem('moonbite_user_id') || 'anon'); } function getContacts() { try { return JSON.parse(localStorage.getItem(contactsKey()) || '[]'); } catch (e) { return []; } } function renderContacts(filter) { const list = document.getElementById('contactsList'); const empty = document.getElementById('contactsEmpty'); if (!list) return; const q = (filter || '').trim().toLowerCase(); const items = getContacts().filter(c => !q || c.name.toLowerCase().includes(q) || c.address.toLowerCase().includes(q)); list.innerHTML = ''; if (!items.length) { if (empty) empty.style.display = getContacts().length ? 'none' : 'block'; return; } if (empty) empty.style.display = 'none'; items.forEach(c => { const initials = c.name.trim().split(/\s+/).map(w => w[0]).join('').slice(0,2).toUpperCase() || '?'; const row = document.createElement('div'); row.className = 'contact-item'; row.style.cursor = 'pointer'; row.onclick = () => { // Tapping a contact prefills the send form. const r = document.getElementById('sendRecipient'); if (r) r.value = c.address; showScreen('sendScreen'); }; const av = document.createElement('div'); av.className = 'contact-avatar'; av.textContent = initials; const info = document.createElement('div'); info.className = 'contact-info'; const nm = document.createElement('div'); nm.className = 'contact-name'; nm.textContent = c.name; const ad = document.createElement('div'); ad.className = 'contact-address'; ad.textContent = c.address; info.appendChild(nm); info.appendChild(ad); row.appendChild(av); row.appendChild(info); list.appendChild(row); }); } function saveContact() { const name = (document.getElementById('newContactName')?.value || '').trim(); const address = (document.getElementById('newContactAddress')?.value || '').trim(); if (!name) { alert('Enter a name for this contact.'); return; } if (!window.MoonBiteAddress || !window.MoonBiteAddress.isValidAddress(address)) { alert('Enter a valid MBITE address.'); return; } const contacts = getContacts(); contacts.push({ name, address }); localStorage.setItem(contactsKey(), JSON.stringify(contacts)); document.getElementById('newContactName').value = ''; document.getElementById('newContactAddress').value = ''; showScreen('contactsScreen'); } /* --- Activity log (transaction history) --- There is no per-address history endpoint on this chain, so the log is kept locally per wallet: sends are recorded when broadcast and confirmed by polling the explorer; incoming MBITE is detected as a net balance increase on refresh. Honest for a non-custodial wallet — it reflects what this device has seen, and clearing storage clears it. */ function activityKey() { return 'moonbite_activity_' + (localStorage.getItem('moonbite_user_id') || 'anon'); } function getActivity() { try { return JSON.parse(localStorage.getItem(activityKey()) || '[]'); } catch (e) { return []; } } function saveActivity(a) { localStorage.setItem(activityKey(), JSON.stringify(a.slice(0, 200))); } function logActivity(entry) { const a = getActivity(); a.unshift(Object.assign({ ts: Math.floor(Date.now() / 1000) }, entry)); saveActivity(a); } function fmtWhen(ts) { const d = new Date(ts * 1000); return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); } function renderActivity() { const list = document.getElementById('activityList'); const empty = document.getElementById('activityEmpty'); if (!list) return; const items = getActivity(); list.innerHTML = ''; if (!items.length) { if (empty) empty.style.display = 'block'; return; } if (empty) empty.style.display = 'none'; items.forEach(it => { const sent = it.type === 'send'; const row = document.createElement('div'); row.className = 'contact-item'; row.style.justifyContent = 'space-between'; const left = document.createElement('div'); left.className = 'contact-info'; const title = document.createElement('div'); title.className = 'contact-name'; title.textContent = sent ? 'Sent' : 'Received'; const sub = document.createElement('div'); sub.className = 'contact-address'; sub.textContent = fmtWhen(it.ts) + (it.status ? ' · ' + it.status : '') + (it.txid ? ' · ' + it.txid.slice(0, 10) + '…' : ''); left.appendChild(title); left.appendChild(sub); const amt = document.createElement('div'); amt.style.fontFamily = 'var(--font-data, ui-monospace, monospace)'; amt.style.fontWeight = '700'; amt.style.whiteSpace = 'nowrap'; amt.style.color = sent ? 'var(--text-primary)' : 'var(--emerald, #2E9E6B)'; amt.textContent = (sent ? '-' : '+') + mbite(it.amountUnits) + ' MBITE'; row.appendChild(left); row.appendChild(amt); list.appendChild(row); }); } async function refreshActivityConfirmations() { const a = getActivity(); let changed = false; for (const it of a) { if (it.type === 'send' && it.status === 'pending' && it.txid) { try { const r = await fetch('/api/explorer/tx/' + it.txid); if (r.ok) { const d = await r.json(); // The confirmation status is on the nested tx, not // the API envelope's top-level "success". const tx = d && d.transaction; if (tx && (tx.status === 'confirmed' || tx.block_height != null)) { it.status = 'confirmed'; changed = true; } } } catch (e) { /* offline: leave pending */ } } } if (changed) { saveActivity(a); renderActivity(); } } /* Poll a just-sent tx and reflect confirmation on the success screen. Stops on confirmation, when the user leaves the screen, or after a bounded number of tries so it never runs forever. */ let _successPollTimer = null; function startSuccessConfirmationPoll(txid) { if (_successPollTimer) { clearInterval(_successPollTimer); _successPollTimer = null; } const line = document.getElementById('successStatusLine'); let tries = 0; const stop = () => { clearInterval(_successPollTimer); _successPollTimer = null; }; _successPollTimer = setInterval(async () => { tries++; // Give up quietly after ~5 minutes, or if the user navigated away. if (tries > 60 || !document.getElementById('transactionSuccessScreen').classList.contains('active')) { stop(); return; } try { const r = await fetch('/api/explorer/tx/' + txid); if (!r.ok) return; const d = await r.json(); const tx = d && d.transaction; if (tx && (tx.status === 'confirmed' || tx.block_height != null)) { if (line) line.textContent = 'Confirmed in block ' + tx.block_height; // Reflect it in the activity log too. const a = getActivity(); const e = a.find(x => x.txid === txid); if (e && e.status !== 'confirmed') { e.status = 'confirmed'; saveActivity(a); } stop(); } } catch (e) { /* offline: keep waiting */ } }, 5000); } /* --- Send: available balance and Max --- */ function currentFeeUnits() { const feeText = (document.getElementById('networkFeeDisplay')?.textContent || '0') .replace(/[^0-9.]/g, ''); return toCents(feeText) || 0; } function updateSendAvailable() { const el = document.getElementById('sendAvailable'); if (!el) return; const bal = CustodialWallet.getWallet().balanceCents || 0; el.textContent = 'Available: ' + mbite(bal) + ' MBITE'; } /* "Review & Send" used to be a bare screen switch (data-act="screen"), with no check on what was typed - an invalid address or an amount the wallet does not have would sail straight through onto a screen headed "THIS CANNOT BE UNDONE", and only fail (via alert, after typing the confirmation amount) on the final destructive click. The real checks belong here, before the user is told to double-check anything. sendTransaction() still re-validates independently before it ever signs - this is an earlier, friendlier gate, not the only one. */ function reviewAndSend() { const errEl = document.getElementById('sendReviewError'); const fail = (msg) => { if (errEl) errEl.textContent = msg; }; if (errEl) errEl.textContent = ''; if (!window.MoonBiteAddress) return fail('Wallet is still loading. Please try again in a moment.'); const recipient = (document.getElementById('sendRecipient').value || '').trim(); if (!recipient) return fail('Enter a recipient address.'); if (!window.MoonBiteAddress.isValidAddress(recipient)) { return fail('That recipient address is not valid. Check it and try again.'); } const wallet = CustodialWallet.getWallet(); if (wallet && recipient === wallet.address) return fail('That is your own address.'); const amountUnits = toCents(document.getElementById('sendAmount').value); if (!amountUnits || amountUnits <= 0) return fail('Enter an amount greater than zero.'); const bal = (wallet && wallet.balanceCents) || 0; const feeUnits = currentFeeUnits(); if (amountUnits + feeUnits > bal) return fail('Not enough MBITE for that amount plus the network fee.'); const fmt = (units) => (units / 1e8).toFixed(8).replace(/0+$/, '').replace(/\.$/, '') + ' MBITE'; document.getElementById('confirmRecipient').textContent = recipient; document.getElementById('confirmAmount').textContent = fmt(amountUnits); document.getElementById('confirmFee').textContent = fmt(feeUnits); document.getElementById('confirmTotal').textContent = fmt(amountUnits + feeUnits); const targetEl = document.getElementById('sendConfirmTarget'); if (targetEl) targetEl.textContent = (amountUnits / 1e8).toFixed(8).replace(/0+$/, '').replace(/\.$/, ''); const sc = document.getElementById('sendConfirmInput'); if (sc) sc.value = ''; showScreen('confirmSendScreen'); } function setMaxSendAmount() { const bal = CustodialWallet.getWallet().balanceCents || 0; const maxUnits = Math.max(0, bal - currentFeeUnits()); // Spend everything the fee leaves behind — trim trailing zeros so // the field reads cleanly. document.getElementById('sendAmount').value = maxUnits ? (maxUnits / 1e8).toFixed(8).replace(/0+$/, '').replace(/\.$/, '') : '0'; updateSendAvailable(); } // Copy Address Function (this later definition is the effective one). // Uses the wallet's own derived, checksum-valid address only -- never a // placeholder, which would send received funds somewhere unspendable. function copyAddress() { const address = _realWalletAddress(); if (!address) { alert('Unlock your wallet first to reveal your receive address.'); return; } if (navigator.clipboard) { navigator.clipboard.writeText(address).then(() => { alert('Address copied to clipboard!'); }).catch(() => { alert('Address: ' + address); }); } else { alert('Address: ' + address); } } // Password strength indicator function updatePasswordStrength() { const password = document.getElementById('passwordInput')?.value || ''; let strength = 0; if (password.length >= 8) strength++; if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++; if (/\d/.test(password)) strength++; if (/[!@#$%^&*]/.test(password)) strength++; // Update strength bars for (let i = 1; i <= 4; i++) { const bar = document.getElementById(`strengthBar${i}`); if (bar) { bar.style.background = i <= strength ? 'var(--success)' : 'rgba(74, 222, 128, 0.3)'; } } } function completeOnboarding() { // Validate passwords match const password = document.getElementById('passwordInput')?.value || ''; const confirmPassword = document.getElementById('confirmPasswordInput')?.value || ''; if (password !== confirmPassword) { alert('Passwords do not match!'); return; } if (password.length < 8) { alert('Password must be at least 8 characters!'); return; } // Mark onboarding as complete localStorage.setItem('moonbite_onboarding_complete', 'true'); localStorage.setItem('moonbite_wallet_setup', 'true'); // Show dashboard showScreen('dashboardScreen'); setActiveNav(document.querySelectorAll('.nav-item')[0]); // Show success message alert('Wallet setup complete! Welcome to MoonBite.'); } // Auto-submit when PIN fallback is complete (all 6 digits entered) function handlePinFallbackComplete() { const pin0 = document.getElementById('verifyPin0').value; const pin1 = document.getElementById('verifyPin1').value; const pin2 = document.getElementById('verifyPin2').value; const pin3 = document.getElementById('verifyPin3').value; const pin4 = document.getElementById('verifyPin4').value; const pin5 = document.getElementById('verifyPin5').value; if (pin0 && pin1 && pin2 && pin3 && pin4 && pin5) { verifyPINAndLogin(); } } function handleNewPinComplete() { const pin0 = document.getElementById('newPin0').value; const pin1 = document.getElementById('newPin1').value; const pin2 = document.getElementById('newPin2').value; const pin3 = document.getElementById('newPin3').value; const pin4 = document.getElementById('newPin4').value; const pin5 = document.getElementById('newPin5').value; // Allow manual submission by user - don't auto-trigger if (pin0 && pin1 && pin2 && pin3 && pin4 && pin5) { const btn = document.querySelector('[data-act="call"][data-fn="saveRecoveredPIN"]'); if (btn) btn.style.background = 'linear-gradient(135deg, var(--primary), var(--primary-dark))'; } } // Verify user-provided seed phrase async function verifySeedPhrase() { const seedInput = document.getElementById('seedPhraseInput').value.trim(); const errorDiv = document.getElementById('seedError'); // Parse words const words = seedInput.split(/\s+/).filter(w => w.length > 0); // Standard BIP39: 12/15/18/21/24 words with a valid checksum. if (![12, 15, 18, 21, 24].includes(words.length)) { errorDiv.textContent = `A recovery phrase is 12 words (you entered ${words.length}).`; errorDiv.style.display = 'block'; return; } const okPhrase = window.MoonBitePhrase && await window.MoonBitePhrase.validateMnemonic(seedInput); if (!okPhrase) { errorDiv.textContent = 'That is not a valid recovery phrase — check the words and their order.'; errorDiv.style.display = 'block'; return; } errorDiv.style.display = 'none'; // Hold the phrase in memory only until the PIN step encrypts it _pendingSeed = seedInput; // Move to PIN setup showScreen('pinSetupScreen'); } /* Hide the splash once the crypto modules are loaded. A timeout fallback guarantees it can never trap the user if a module stalls. */ (function initSplash() { const splash = document.getElementById('splashScreen'); if (!splash) return; const hide = () => { splash.style.opacity = '0'; setTimeout(() => { splash.style.display = 'none'; }, 450); }; if (window.MoonBiteReady) { hide(); return; } window.addEventListener('moonbite-ready', hide, { once: true }); setTimeout(hide, 6000); })(); /* --- New-wallet creation (the "Create a New Wallet" fork) --- */ let _generatedPhrase = ''; async function createNewWallet() { if (!window.MoonBitePhrase) { alert('Wallet is still loading — please try again in a moment.'); return; } // A freshly generated, high-entropy phrase for the user's one wallet. _generatedPhrase = await window.MoonBitePhrase.generatePhrase(); const disp = document.getElementById('generatedPhraseDisplay'); if (disp) disp.textContent = _generatedPhrase; const cb = document.getElementById('phraseBackedUp'); if (cb) cb.checked = false; const btn = document.getElementById('phraseContinueBtn'); if (btn) btn.disabled = true; showScreen('createWalletScreen'); } function copyGeneratedPhrase() { if (!_generatedPhrase) return; const done = () => alert('Recovery phrase copied. Store it somewhere safe, then delete the copy.'); if (navigator.clipboard) { navigator.clipboard.writeText(_generatedPhrase).then(done) .catch(() => alert('Copy failed — please write the words down manually.')); } else { alert('Your phrase:\n\n' + _generatedPhrase); } } let _backupChallenge = []; function confirmGeneratedPhrase() { if (!_generatedPhrase) { showScreen('walletSetupScreen'); return; } // Prove the phrase was actually written down before it becomes the // only key to the wallet: ask for three of the twelve words by // position. A checkbox alone let people skip the backup and lose // their funds. const words = _generatedPhrase.split(/\s+/); const positions = []; while (positions.length < 3) { const p = window.MoonBiteAddress ? (unbiasedSmall(words.length)) : Math.floor(Math.random() * words.length); if (!positions.includes(p)) positions.push(p); } positions.sort((a, b) => a - b); _backupChallenge = positions; const host = document.getElementById('verifyBackupPrompts'); host.innerHTML = ''; positions.forEach(p => { const g = document.createElement('div'); g.className = 'form-group'; const l = document.createElement('label'); l.className = 'form-label'; l.textContent = 'Word #' + (p + 1); const inp = document.createElement('input'); inp.type = 'text'; inp.className = 'form-input'; inp.autocomplete = 'off'; inp.autocapitalize = 'off'; inp.spellcheck = false; inp.dataset.pos = String(p); inp.placeholder = 'the ' + ordinal(p + 1) + ' word'; g.appendChild(l); g.appendChild(inp); host.appendChild(g); }); document.getElementById('verifyBackupError').style.display = 'none'; showScreen('verifyBackupScreen'); } function checkPhraseBackup() { const words = _generatedPhrase.split(/\s+/); const inputs = document.querySelectorAll('#verifyBackupPrompts input'); let ok = inputs.length === _backupChallenge.length; inputs.forEach(inp => { const pos = parseInt(inp.dataset.pos, 10); if ((inp.value || '').trim().toLowerCase() !== words[pos]) ok = false; }); if (!ok) { document.getElementById('verifyBackupError').style.display = 'block'; return; } // Verified: hand the phrase to the PIN step, which encrypts it. _pendingSeed = _generatedPhrase; showScreen('pinSetupScreen'); } function ordinal(n) { const s = ['th', 'st', 'nd', 'rd'], v = n % 100; return n + (s[(v - 20) % 10] || s[v] || s[0]); } function unbiasedSmall(max) { const limit = Math.floor(0x100000000 / max) * max; const b = new Uint32Array(1); let x; do { crypto.getRandomValues(b); x = b[0]; } while (x >= limit); return x % max; } /* --- Reveal the recovery phrase again, PIN-gated (from Security) --- */ async function revealRecoveryPhrase() { const stored = localStorage.getItem('moonbite_seed_encrypted'); if (!stored) { alert('No wallet on this device.'); return; } const blocked = pinGuardBlockedMessage(); if (blocked) { alert(blocked); return; } const pin = await requestPinAsync('Enter your PIN to view your recovery phrase'); if (!pin) return; let seed = null; try { seed = await CustodialWallet.decryptSeedWithPIN(stored, pin); } catch (e) { alert('Could not unlock — reload and try again.'); return; } if (!seed) { alert(pinGuardFail()); return; } pinGuard().reset(); document.getElementById('revealedPhrase').textContent = seed; showScreen('revealPhraseScreen'); } function hideRevealedPhrase() { // Do not leave the plaintext phrase sitting in the DOM. const el = document.getElementById('revealedPhrase'); if (el) el.textContent = ''; showScreen('securityScreen'); } // Verify recovery seed phrase for PIN reset async function verifyRecoverySeed() { const seedInput = document.getElementById('recoveryPhraseInput').value.trim(); const errorDiv = document.getElementById('recoveryError'); const encryptedSeed = localStorage.getItem('moonbite_seed_encrypted'); if (!encryptedSeed) { errorDiv.textContent = 'No seed phrase found in storage'; errorDiv.style.display = 'block'; return; } // Parse words const words = seedInput.split(/\s+/).filter(w => w.length > 0); // Standard BIP39 phrase (12 words) with a valid checksum. if (![12, 15, 18, 21, 24].includes(words.length)) { errorDiv.textContent = `A recovery phrase is 12 words (you entered ${words.length}).`; errorDiv.style.display = 'block'; return; } if (!(window.MoonBitePhrase && await window.MoonBitePhrase.validateMnemonic(seedInput))) { errorDiv.textContent = 'That is not a valid recovery phrase.'; errorDiv.style.display = 'block'; return; } /* Prove the entered phrase belongs to the wallet ALREADY on this device before we let the next step overwrite its encrypted seed. This is the "forgot my PIN" path (it only runs when a blob is present), so the phrase must derive the SAME address. Skipping this check let a different-but-checksum-valid phrase silently destroy the original seed (fund loss if never backed up) or let someone with brief device access plant their own wallet. */ let enteredAddr = null; try { const d = window.MoonBiteAddress && await window.MoonBiteAddress.deriveFromSeedPhrase(seedInput); enteredAddr = d && d.address; } catch (e) { /* leave null -> treated as mismatch below */ } const currentAddr = (CustodialWallet.getWallet() || {}).address || ''; if (currentAddr && currentAddr.startsWith('moon1') && enteredAddr !== currentAddr) { const ok = confirm( 'This recovery phrase belongs to a DIFFERENT wallet than the ' + 'one on this device.\n\nContinuing will REPLACE the wallet ' + 'stored here. If the current wallet’s recovery phrase is ' + 'not backed up elsewhere, its funds will be permanently lost.' + '\n\nReplace the wallet on this device?'); if (!ok) { errorDiv.textContent = 'Recovery cancelled — the wallet on this device was not changed.'; errorDiv.style.display = 'block'; return; } /* User accepted the replacement: keep the old blob under a backup key so a mistaken replace is still recoverable. */ try { localStorage.setItem('moonbite_seed_encrypted_backup', encryptedSeed); localStorage.setItem('moonbite_seed_backup_ts', String(Date.now())); } catch (e) { /* storage full/unavailable: proceed anyway */ } } // Hold the recovered phrase in memory only until the new PIN step _recoverySeed = seedInput; errorDiv.style.display = 'none'; // Move to new PIN setup showScreen('newPINSetupScreen'); } // Save new PIN after successful recovery async function saveRecoveredPIN() { const newPin = document.getElementById('newPin0').value + document.getElementById('newPin1').value + document.getElementById('newPin2').value + document.getElementById('newPin3').value + document.getElementById('newPin4').value + document.getElementById('newPin5').value; if (newPin.length !== 6) { alert('Please enter a valid 6-digit PIN'); return; } const recoveredSeed = _recoverySeed; if (!recoveredSeed) { alert('Recovery failed. Please try again.'); showScreen('loginScreen'); return; } // Re-encrypt seed with new PIN const encryptedSeed = await CustodialWallet.encryptSeedWithPIN(recoveredSeed, newPin); localStorage.removeItem('moonbite_pin'); localStorage.setItem('moonbite_seed_encrypted', encryptedSeed); pinGuard().reset(); // a proven recovery phrase clears any lockout /* Recovering on a new device: derive the address from the seed the user just proved they hold, so their existing funds appear. */ CustodialWallet.deriveAddressFromSeed(recoveredSeed) .then(() => CustodialWallet.refreshBalance()); clearOnboardingSecrets(); // Show success const btn = document.querySelector('[data-act="call"][data-fn="saveRecoveredPIN"]'); if (btn) { btn.textContent = 'PIN Reset!'; btn.style.background = 'linear-gradient(135deg, #2E9E6B, #2E9E6B)'; } // Return to login setTimeout(() => { showScreen('loginScreen'); }, 500); } // Populate wallets screen with user's actual wallet (not demo data) function populateWalletsScreen() { const wallet = CustodialWallet.getWallet(); const container = document.getElementById('walletsContainer'); if (!container) return; /* Built with textContent rather than an innerHTML template so a wallet name can never be interpreted as markup — harmless today because the name is fixed, but a renaming feature would turn this into self-XSS. */ container.innerHTML = ''; const item = document.createElement('div'); item.className = 'wallet-item'; const nameEl = document.createElement('div'); nameEl.className = 'wallet-name'; nameEl.textContent = wallet.walletName; const balEl = document.createElement('div'); balEl.className = 'wallet-balance'; balEl.textContent = mbite(wallet.balanceCents) + ' MBITE'; const addrEl = document.createElement('div'); addrEl.style.cssText = 'font-size: 11px; color: var(--text-secondary); margin-top: 4px;'; addrEl.textContent = wallet.address || 'Unlock your wallet to reveal your address'; item.appendChild(nameEl); item.appendChild(balEl); item.appendChild(addrEl); const btn = document.createElement('button'); btn.className = 'btn btn-secondary'; btn.style.cssText = 'width: 100%; margin-top: var(--spacing-lg);'; btn.textContent = 'View Details →'; btn.onclick = () => showScreen('dashboardScreen'); container.appendChild(item); container.appendChild(btn); } // PIN verification for returning users (fallback option) async function verifyPINAndLogin() { const enteredPIN = document.getElementById('verifyPin0').value + document.getElementById('verifyPin1').value + document.getElementById('verifyPin2').value + document.getElementById('verifyPin3').value + document.getElementById('verifyPin4').value + document.getElementById('verifyPin5').value; if (enteredPIN.length !== 6) { alert('Please enter a valid 6-digit PIN'); return; } /* The PIN is verified by decrypting the seed with it. There is no stored PIN to compare against — the previous build kept one in localStorage in the clear, beside the seed it guarded. */ const encryptedSeed = localStorage.getItem('moonbite_seed_encrypted'); const blockedMsg = pinGuardBlockedMessage(); if (blockedMsg) { const err = document.getElementById('pinVerifyError'); if (err) { err.textContent = blockedMsg; err.style.display = 'block'; } for (let i = 0; i < 6; i++) document.getElementById('verifyPin' + i).value = ''; return; } let seed = null; try { seed = await CustodialWallet.decryptSeedWithPIN(encryptedSeed, enteredPIN); } catch (e) { // Never leave the screen frozen: a decrypt that throws (e.g. the // crypto module failing to load) shows a clear message instead. const err = document.getElementById('pinVerifyError'); if (err) { err.textContent = 'Could not unlock — please reload the page and try again.'; err.style.display = 'block'; } return; } if (seed) { pinGuard().reset(); document.getElementById('pinVerifyError').style.display = 'none'; /* Unlock is the only moment the plaintext seed is available on a returning visit, so it is where a wallet still carrying the old placeholder address gets its real one. */ CustodialWallet.ensureRealAddress(seed) .then(() => CustodialWallet.refreshBalance()); showScreen('dashboardScreen'); setActiveNav(document.querySelectorAll('.nav-item')[0]); /* Legacy weak-entropy ("brainwallet") detection. A stored phrase that is not valid BIP39 was turned into a key by sha256(prefix+phrase) (moonbite-address.js), which is guessable by offline enumeration. Onboarding now rejects such phrases, but a pre-existing wallet can still hold one. Warn loudly and flag it so funds get moved to a real BIP39 wallet. Best-effort: never blocks unlock. */ try { const isBip39 = window.MoonBitePhrase && await window.MoonBitePhrase.validateMnemonic(seed); if (!isBip39) { localStorage.setItem('moonbite_legacy_weak_seed', '1'); if (!sessionStorage.getItem('moonbite_legacy_warned')) { sessionStorage.setItem('moonbite_legacy_warned', '1'); alert('SECURITY WARNING\n\nThis wallet’s recovery ' + 'phrase is a legacy, weak-entropy phrase that an ' + 'attacker can guess. Create a NEW wallet and move ' + 'any funds to it as soon as possible, and do not ' + 'receive new MBITE to this wallet.'); } } else { localStorage.removeItem('moonbite_legacy_weak_seed'); } } catch (e) { /* detection is best-effort; never block unlock */ } } else { // PIN is incorrect - show error, count the failure (backoff / wipe) const msg = pinGuardFail(); const errEl = document.getElementById('pinVerifyError'); errEl.textContent = msg; errEl.style.display = 'block'; // Clear the inputs document.getElementById('verifyPin0').value = ''; document.getElementById('verifyPin1').value = ''; document.getElementById('verifyPin2').value = ''; document.getElementById('verifyPin3').value = ''; document.getElementById('verifyPin4').value = ''; document.getElementById('verifyPin5').value = ''; document.getElementById('verifyPin0').focus(); } } // Auto-focus next pin input document.addEventListener('DOMContentLoaded', () => { const pinInputs = document.querySelectorAll('#pinVerifyScreen .pin-input, #pinSetupScreen .pin-input'); pinInputs.forEach((input, index) => { input.addEventListener('input', (e) => { if (e.target.value && index < pinInputs.length - 1) { pinInputs[index + 1].focus(); } }); input.addEventListener('keydown', (e) => { if (e.key === 'Backspace' && !e.target.value && index > 0) { pinInputs[index - 1].focus(); } }); }); }); // Check if onboarding was already completed // ============================================================ // Theme Toggle System // ============================================================ function initTheme() { const savedTheme = localStorage.getItem('moonbite_theme') || 'dark'; setTheme(savedTheme); } function setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('moonbite_theme', theme); const themeToggleBtn = document.getElementById('themeToggle'); if (themeToggleBtn) { // Swap the icon markup; textContent would erase the SVG. themeToggleBtn.innerHTML = theme === 'light' ? '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.8A9 9 0 1111.2 3a7 7 0 009.8 9.8z"/></svg>' : '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>'; } } function toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark'; const newTheme = currentTheme === 'light' ? 'dark' : 'light'; setTheme(newTheme); } function checkOnboardingStatus() { const onboardingComplete = localStorage.getItem('moonbite_onboarding_complete'); if (onboardingComplete) { // Skip onboarding, show dashboard const pinSetupScreen = document.getElementById('pinSetupScreen'); const dashboardScreen = document.getElementById('dashboardScreen'); if (pinSetupScreen) pinSetupScreen.classList.remove('active'); if (dashboardScreen) dashboardScreen.classList.add('active'); setActiveNav(document.querySelectorAll('.nav-item')[0]); } } // Initialize onboarding check on page load // Note: onboarding check is now handled in main load event above // window.addEventListener('load', () => { // initTheme(); // setTimeout(checkOnboardingStatus, 100); // }); // Also check immediately initTheme(); checkOnboardingStatus(); </script></body></html>