545 lines
19 KiB
JavaScript
545 lines
19 KiB
JavaScript
(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);
|
||
})();
|