(function () { var ui = window.FamiCenterUI; var identity = String(ui.params.get('identity') || 'HOST').toUpperCase(); var explicitChannel = ui.params.get('channel'); var state = { channel: explicitChannel === 'address' ? 'address' : 'dealer', diamondBalance: 0, ratio: 1000000, minimumPoints: 0, feeBPS: 0, dealers: [], selectedDealerId: '', address: '', profileAvatar: '', contact: '', submitting: false, savingAddress: false, }; var contactController = null; var mock = { overview: { asset_type: 'POINT', balance: { available_amount: 1000000000 }, withdraw_address: { withdraw_usdt_trc20_address: '' }, points_per_usd: 1000000, fee_bps: 0, minimum_points: 1000000, coin_sellers: Array.from({ length: 3 }, function (_, index) { return { user_id: 'seller-' + (index + 1), display_user_id: '123456789', nickname: 'konghuimingc', avatar: '../../products/fami/centers/assets/fami/agency-avatar.png', }; }), }, user: { avatar: '../../products/fami/centers/assets/fami/agency-avatar.png', contact_info: '+65 1234 5678', }, }; function normalizeIdentity(value) { var normalized = String(value || '').toUpperCase(); if (normalized === 'AGENCY' || normalized === 'BD') { return normalized; } return 'HOST'; } function paymentAddress(payload) { var data = ui.unwrap(payload) || {}; var source = data.withdraw_address || data; return String( source.withdraw_usdt_trc20_address || source.usdt_trc20_address || '' ).trim(); } function normalizeDealer(item) { item = item || {}; return { id: String(item.user_id || ''), displayId: String(item.display_user_id || ''), name: String(item.nickname || 'Dealer'), avatar: String(item.avatar || ''), }; } function amountUSD() { return ui.number(String(ui.$('withdrawAmount').value || '').trim()); } function amountPoints() { var text = String(ui.$('withdrawAmount').value || '').trim(); if (!/^\d+(?:\.\d{0,2})?$/.test(text)) return 0; var parts = text.split('.'); var usdMinor = ui.number(parts[0]) * 100 + ui.number((parts[1] || '').padEnd(2, '0')); // 输入框按 Figma 使用美元符号,账本却只接收整数 POINT;用分为单位换算可避免浮点金额跨端不一致。 return Math.round((usdMinor * state.ratio) / 100); } function validTrc20(value) { return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(String(value || '').trim()); } function applyOverview(payload) { var data = ui.unwrap(payload) || {}; var balance = data.balance || {}; state.diamondBalance = ui.number(balance.available_amount); state.ratio = ui.number(data.points_per_usd) || 1000000; state.minimumPoints = ui.number(data.minimum_points); state.feeBPS = ui.number(data.fee_bps); state.dealers = Array.isArray(data.coin_sellers) ? data.coin_sellers.map(normalizeDealer).filter(function (item) { return item.id && item.displayId; }) : []; if ( !state.dealers.some(function (dealer) { return dealer.id === state.selectedDealerId; }) ) { state.selectedDealerId = state.dealers.length ? state.dealers[0].id : ''; } state.address = paymentAddress(data); if (!explicitChannel) { state.channel = state.dealers.length ? 'dealer' : 'address'; } } function renderTabs() { document.querySelectorAll('.withdraw-tab').forEach(function (tab) { var active = tab.getAttribute('data-mode') === 'usdt'; tab.classList.toggle('is-active', active); tab.setAttribute('aria-selected', String(active)); }); } function renderDealerList() { var list = ui.$('dealerList'); list.replaceChildren(); state.dealers.slice(0, 3).forEach(function (dealer) { var card = document.createElement('button'); card.className = 'dealer-card'; card.type = 'button'; card.setAttribute( 'aria-pressed', String(dealer.id === state.selectedDealerId) ); var avatar = document.createElement('img'); avatar.className = 'dealer-card__avatar'; avatar.alt = ''; ui.setAvatar( avatar, dealer.avatar, '../../products/fami/centers/assets/fami/agency-avatar.png' ); var copy = document.createElement('span'); copy.className = 'dealer-card__copy'; var name = document.createElement('strong'); name.textContent = dealer.name; var id = document.createElement('span'); id.textContent = 'ID:' + dealer.displayId; var radio = document.createElement('img'); radio.className = 'dealer-card__radio'; radio.alt = ''; radio.src = dealer.id === state.selectedDealerId ? '../../products/fami/centers/assets/fami/radio-selected.svg' : '../../products/fami/centers/assets/fami/radio-unselected.svg'; copy.appendChild(name); copy.appendChild(id); card.appendChild(avatar); card.appendChild(copy); card.appendChild(radio); card.addEventListener('click', function () { state.selectedDealerId = dealer.id; renderDealerList(); updateButtons(); }); list.appendChild(card); }); if (!state.dealers.length) { var empty = document.createElement('p'); empty.className = 'fami-empty'; empty.textContent = ui.t( 'fami.no_currency_dealer', 'No currency dealer' ); list.appendChild(empty); } } function renderAddress() { ui.setAvatar( ui.$('addressAvatar'), state.profileAvatar, '../../products/fami/centers/assets/fami/agency-avatar.png' ); ui.$('addressTitle').textContent = state.address ? 'USDT TRC20' : ui.t('withdraw_exchange.no_payment_info', 'No USDT address'); ui.$('addressText').textContent = state.address ? state.address : ui.t( 'fami.no_address_copy', 'Fill in a TRC20 USDT address before withdrawing.' ); ui.$('addressAddButton').hidden = !!state.address; ui.$('addressAddButton').setAttribute( 'aria-label', ui.t('withdraw_exchange.save_address', 'Save address') ); } function updateButtons() { var points = amountPoints(); var commonDisabled = state.submitting || points <= 0 || points > state.diamondBalance || (state.minimumPoints > 0 && points < state.minimumPoints); // Dealer 参考稿在空金额时仍为主色;点击后由 validateSubmit 解释原因,地址真实提交则保持不可用直到金额和地址都有效。 ui.$('dealerSubmitButton').disabled = state.submitting || !state.selectedDealerId; ui.$('addressSubmitButton').disabled = commonDisabled || !state.address; var text = state.submitting ? ui.t('withdraw_exchange.submitting', 'Submitting...') : ui.t('fami.withdrawal', 'Withdrawal'); ui.$('dealerSubmitButton').textContent = text; ui.$('addressSubmitButton').textContent = text; } function renderViews() { ui.$('dealerView').hidden = state.channel !== 'dealer'; ui.$('addressView').hidden = state.channel !== 'address'; ui.$('goldView').hidden = true; } function render() { renderTabs(); ui.$('diamondBalance').textContent = ui.formatNumber( state.diamondBalance ); ui.$('withdrawRatio').textContent = Math.round(state.ratio) + '=$1'; renderViews(); renderDealerList(); renderAddress(); updateButtons(); if (contactController) contactController.setContact(state.contact); } function load() { if (ui.isMock()) { applyOverview(mock.overview); state.profileAvatar = ui.profileFrom(mock.user, 'User').avatar; state.contact = ui.contactFrom(mock.user); if (explicitChannel === 'address') state.channel = 'address'; render(); return; } var api = window.HyAppAPI; if (!api || !api.pointWallet || !api.salaryWallet) { render(); ui.toast( ui.t( 'withdraw_exchange.load_failed', 'Load failed. Try again later.' ) ); return; } // POINT 总览一次返回真实余额、政策比例、最低门槛、地址和白名单币商;联系方式单独读取用户资料,不能再用工资 USD 推导钻石。 Promise.all([ ui.safe('point-wallet-overview', function () { return api.pointWallet.overview(); }), ui.safe('withdraw-contact', function () { return api.user.me(); }), ]).then(function (results) { if (results[0]) applyOverview(results[0]); var profile = results[1] ? ui.profileFrom(results[1], 'User') : null; state.profileAvatar = profile ? profile.avatar : ''; state.contact = profile ? profile.contact : ''; render(); if (!results.some(Boolean)) { ui.toast( ui.t( 'withdraw_exchange.load_failed', 'Load failed. Try again later.' ) ); } }); } function normalizeAmount() { var input = ui.$('withdrawAmount'); var value = String(input.value || '') .replace(/[^0-9.]/g, '') .replace(/(\..*)\./g, '$1'); var parts = value.split('.'); if (parts.length > 1) value = parts[0] + '.' + parts[1].slice(0, 2); input.value = value; updateButtons(); } function setAddressStatus(message, success) { var node = ui.$('addressStatus'); node.textContent = message || ''; node.hidden = !message; node.classList.toggle('is-success', !!success); } function openAddressEditor() { ui.$('addressInput').value = state.address; setAddressStatus(''); ui.openModal('addressModal'); window.setTimeout(function () { ui.$('addressInput').focus(); }, 0); } function saveAddress(event) { event.preventDefault(); var value = String(ui.$('addressInput').value || '').trim(); if (!validTrc20(value)) { setAddressStatus( ui.t( 'withdraw_exchange.invalid_address', 'Please enter a valid TRC20 USDT address' ) ); return; } if (ui.isMock()) { state.address = value; ui.closeModal('addressModal'); render(); ui.toast(ui.t('withdraw_exchange.address_saved', 'Address saved')); return; } var api = window.HyAppAPI && window.HyAppAPI.salaryWallet; if (!api || !api.saveWithdrawAddress || state.savingAddress) return; state.savingAddress = true; ui.$('addressSaveButton').disabled = true; ui.$('addressSaveButton').textContent = ui.t( 'withdraw_exchange.saving', 'Saving...' ); // 提现地址由用户资料服务持久化;只在服务端确认后更新页面,失败时保留用户输入供修正。 api.saveWithdrawAddress({ usdt_trc20_address: value }) .then(function (response) { state.address = paymentAddress(response) || value; ui.closeModal('addressModal'); render(); ui.toast( ui.t('withdraw_exchange.address_saved', 'Address saved') ); }) .catch(function (error) { setAddressStatus( (error && error.message) || ui.t( 'withdraw_exchange.submit_failed', 'Submit failed.' ) ); }) .finally(function () { state.savingAddress = false; ui.$('addressSaveButton').disabled = false; ui.$('addressSaveButton').textContent = ui.t( 'withdraw_exchange.save_address', 'Save address' ); }); } function selectedDealer() { return state.dealers.find(function (dealer) { return dealer.id === state.selectedDealerId; }); } function validateSubmit() { var usd = amountUSD(); var points = amountPoints(); if (usd <= 0 || points <= 0) { return ui.t( 'withdraw_exchange.invalid_amount', 'Please enter a valid amount' ); } if (points > state.diamondBalance) { return ui.t( 'withdraw_exchange.amount_exceeds_balance', 'Amount exceeds balance' ); } if (state.minimumPoints > 0 && points < state.minimumPoints) { return ui .t( 'withdraw_exchange.minimum_points', 'Minimum withdrawal is {amount} diamonds' ) .replace('{amount}', ui.formatNumber(state.minimumPoints)); } if (state.channel === 'dealer' && !selectedDealer()) { return ui.t( 'fami.select_currency_dealer', 'Select a Currency Dealer' ); } if (state.channel === 'address' && !state.address) { return ui.t( 'withdraw_exchange.missing_payment_info', 'Please add a TRC20 USDT address' ); } return ''; } function requestSubmit(points) { var api = window.HyAppAPI.pointWallet; if (state.channel === 'dealer') { return api.transferToCoinSeller({ command_id: ui.commandId('point-transfer'), seller_user_id: selectedDealer().id, point_amount: points, }); } return api.withdrawUSDT({ command_id: ui.commandId('point-withdraw'), gross_point_amount: points, usdt_trc20_address: state.address, }); } function submit() { var error = validateSubmit(); if (error) { ui.toast(error); return; } if (!state.contact) { ui.$('contactCard').click(); return; } var points = amountPoints(); if (ui.isMock()) { state.diamondBalance = Math.max(0, state.diamondBalance - points); ui.$('withdrawAmount').value = ''; render(); ui.toast( ui.t( 'withdraw_exchange.withdraw_success', 'Withdrawal submitted' ) ); return; } if ( !window.HyAppAPI || !window.HyAppAPI.pointWallet || state.submitting ) return; state.submitting = true; updateButtons(); // 两条资金链返回余额字段不同:币商转账返回 source_point_balance_after,链上提现返回 balance_after 快照;缺失时才本地扣减展示。 requestSubmit(points) .then(function (result) { var after = state.channel === 'dealer' ? result && result.source_point_balance_after : result && result.balance_after && result.balance_after.available_amount; state.diamondBalance = after === undefined || after === null ? Math.max(0, state.diamondBalance - points) : ui.number(after); ui.$('withdrawAmount').value = ''; ui.toast( ui.t( 'withdraw_exchange.withdraw_success', 'Withdrawal submitted' ) ); }) .catch(function (submitError) { ui.toast( (submitError && submitError.message) || ui.t( 'withdraw_exchange.submit_failed', 'Submit failed.' ) ); }) .finally(function () { state.submitting = false; render(); }); } function fallbackURL() { var fallback = '../host-center/'; if (identity === 'AGENCY') fallback = '../agency-center/'; if (identity === 'BD') fallback = '../bd-center/'; // Wallet 回跳只继承产品上下文,不回传金额、渠道等资金页状态。 return window.HyGuild.product.buildURL(fallback); } function init() { identity = normalizeIdentity(identity); contactController = ui.bindContact({ onSaved: function (value) { state.contact = value; }, }); ui.$('backButton').addEventListener('click', function () { ui.goBack(fallbackURL()); }); ui.$('withdrawAmount').addEventListener('input', normalizeAmount); ui.$('dealerSubmitButton').addEventListener('click', submit); ui.$('addressSubmitButton').addEventListener('click', submit); ui.$('addressAddButton').addEventListener('click', openAddressEditor); ui.$('addressCard').addEventListener('click', openAddressEditor); ui.$('addressForm').addEventListener('submit', saveAddress); document .querySelectorAll('[data-address-close]') .forEach(function (node) { node.addEventListener('click', function () { ui.closeModal('addressModal'); }); }); window.addEventListener('hyapp:i18n-ready', render); render(); load(); if (window.HyAppBridge) { window.HyAppBridge.ready({ page: 'fami-wallet', app_code: 'fami', mock: ui.isMock(), }); } } document.addEventListener('DOMContentLoaded', init); })();