feat: add mobile banking app and integrate into phone UI
- Implemented wallet app functionality including account management, transaction handling, and user notifications. - Updated HomeScreen to include Wallet app icon. - Enhanced StateManager to maintain mobile bank state. - Created wallet-specific styles for UI consistency. - Updated CSS and JS concatenation scripts to include wallet resources.
This commit is contained in:
parent
008631ed10
commit
5c76d59baf
@ -3,6 +3,26 @@
|
|||||||
if (isNil QGVAR(BankRepository)) then { call FUNC(initRepository); };
|
if (isNil QGVAR(BankRepository)) then { call FUNC(initRepository); };
|
||||||
if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
|
if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
|
||||||
|
|
||||||
|
GVAR(sendPhoneBankEvent) = {
|
||||||
|
params [["_functionName", "", [""]], ["_arguments", [], [[]]]];
|
||||||
|
|
||||||
|
private _display = uiNamespace getVariable ["RscPhone", displayNull];
|
||||||
|
if (isNull _display || { _functionName isEqualTo "" }) exitWith { false };
|
||||||
|
|
||||||
|
private _control = _display displayCtrl 1001;
|
||||||
|
if (isNull _control) exitWith { false };
|
||||||
|
|
||||||
|
private _serializedArguments = _arguments apply { toJSON _x };
|
||||||
|
private _script = format [
|
||||||
|
"window.%1 && window.%1(%2)",
|
||||||
|
_functionName,
|
||||||
|
_serializedArguments joinString ", "
|
||||||
|
];
|
||||||
|
|
||||||
|
_control ctrlWebBrowserAction ["ExecJS", _script];
|
||||||
|
true
|
||||||
|
};
|
||||||
|
|
||||||
[QGVAR(initBank), {
|
[QGVAR(initBank), {
|
||||||
GVAR(BankRepository) call ["init", []];
|
GVAR(BankRepository) call ["init", []];
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
@ -14,6 +34,7 @@ if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
|
|||||||
if !(isNil QGVAR(BankUIBridge)) then {
|
if !(isNil QGVAR(BankUIBridge)) then {
|
||||||
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
|
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
|
||||||
};
|
};
|
||||||
|
["updateMobileBankAccount", [_data]] call GVAR(sendPhoneBankEvent);
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
|
|
||||||
[QGVAR(responseSyncBank), {
|
[QGVAR(responseSyncBank), {
|
||||||
@ -23,6 +44,7 @@ if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
|
|||||||
if !(isNil QGVAR(BankUIBridge)) then {
|
if !(isNil QGVAR(BankUIBridge)) then {
|
||||||
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
|
GVAR(BankUIBridge) call ["handleAccountSyncResponse", [_data]];
|
||||||
};
|
};
|
||||||
|
["updateMobileBankAccount", [_data]] call GVAR(sendPhoneBankEvent);
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
|
|
||||||
[QGVAR(responseHydrateBank), {
|
[QGVAR(responseHydrateBank), {
|
||||||
@ -31,6 +53,7 @@ if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
|
|||||||
if !(isNil QGVAR(BankUIBridge)) then {
|
if !(isNil QGVAR(BankUIBridge)) then {
|
||||||
GVAR(BankUIBridge) call ["handleHydrateResponse", [_data, "bank::hydrate"]];
|
GVAR(BankUIBridge) call ["handleHydrateResponse", [_data, "bank::hydrate"]];
|
||||||
};
|
};
|
||||||
|
["updateMobileBank", [_data]] call GVAR(sendPhoneBankEvent);
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
|
|
||||||
[QGVAR(responseBankNotice), {
|
[QGVAR(responseBankNotice), {
|
||||||
@ -39,6 +62,7 @@ if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
|
|||||||
if !(isNil QGVAR(BankUIBridge)) then {
|
if !(isNil QGVAR(BankUIBridge)) then {
|
||||||
GVAR(BankUIBridge) call ["handleNoticeResponse", [_type, _message]];
|
GVAR(BankUIBridge) call ["handleNoticeResponse", [_type, _message]];
|
||||||
};
|
};
|
||||||
|
["showMobileBankNotice", [_type, _message]] call GVAR(sendPhoneBankEvent);
|
||||||
}] call CFUNC(addEventHandler);
|
}] call CFUNC(addEventHandler);
|
||||||
|
|
||||||
[{
|
[{
|
||||||
|
|||||||
@ -345,6 +345,56 @@ switch (_event) do {
|
|||||||
|
|
||||||
profileNamespace setVariable ["FORGE_Phone_Alarms", _alarms];
|
profileNamespace setVariable ["FORGE_Phone_Alarms", _alarms];
|
||||||
};
|
};
|
||||||
|
case "phone::bank::refresh": {
|
||||||
|
["forge_server_bank_requestHydrateBank", [getPlayerUID player, "bank", false]] call CFUNC(serverEvent);
|
||||||
|
};
|
||||||
|
case "phone::bank::transfer::request": {
|
||||||
|
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||||
|
private _target = _data getOrDefault ["target", ""];
|
||||||
|
private _from = toLowerANSI (_data getOrDefault ["from", "bank"]);
|
||||||
|
|
||||||
|
if (_target isNotEqualTo "" && { _amount > 0 }) then {
|
||||||
|
["forge_server_bank_requestTransfer", [getPlayerUID player, _target, _from, _amount]] call CFUNC(serverEvent);
|
||||||
|
} else {
|
||||||
|
private _display = uiNamespace getVariable ["RscPhone", displayNull];
|
||||||
|
if !(isNull _display) then {
|
||||||
|
private _control = _display displayCtrl 1001;
|
||||||
|
if !(isNull _control) then {
|
||||||
|
_control ctrlWebBrowserAction ["ExecJS", "window.showMobileBankNotice && window.showMobileBankNotice('error', 'Choose a recipient and valid amount.')"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
case "phone::bank::depositEarnings::request": {
|
||||||
|
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||||
|
|
||||||
|
if (_amount > 0) then {
|
||||||
|
["forge_server_bank_requestDepositEarnings", [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||||
|
} else {
|
||||||
|
private _display = uiNamespace getVariable ["RscPhone", displayNull];
|
||||||
|
if !(isNull _display) then {
|
||||||
|
private _control = _display displayCtrl 1001;
|
||||||
|
if !(isNull _control) then {
|
||||||
|
_control ctrlWebBrowserAction ["ExecJS", "window.showMobileBankNotice && window.showMobileBankNotice('error', 'Enter a valid earnings amount.')"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
case "phone::bank::repayCreditLine::request": {
|
||||||
|
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||||
|
|
||||||
|
if (_amount > 0) then {
|
||||||
|
["forge_server_bank_requestRepayCreditLine", [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||||
|
} else {
|
||||||
|
private _display = uiNamespace getVariable ["RscPhone", displayNull];
|
||||||
|
if !(isNull _display) then {
|
||||||
|
private _control = _display displayCtrl 1001;
|
||||||
|
if !(isNull _control) then {
|
||||||
|
_control ctrlWebBrowserAction ["ExecJS", "window.showMobileBankNotice && window.showMobileBankNotice('error', 'Enter a valid payment amount.')"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
default { hint format ["Unhandled phone event: %1", _event]; };
|
default { hint format ["Unhandled phone event: %1", _event]; };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1529
arma/client/addons/phone/ui/_site/dist/app.bundle.js
vendored
1529
arma/client/addons/phone/ui/_site/dist/app.bundle.js
vendored
File diff suppressed because it is too large
Load Diff
BIN
arma/client/addons/phone/ui/_site/images/dark/Wallet.png
Normal file
BIN
arma/client/addons/phone/ui/_site/images/dark/Wallet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
BIN
arma/client/addons/phone/ui/_site/images/light/Wallet.png
Normal file
BIN
arma/client/addons/phone/ui/_site/images/light/Wallet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
File diff suppressed because one or more lines are too long
@ -90,6 +90,9 @@ class App extends Component {
|
|||||||
case 'settings':
|
case 'settings':
|
||||||
window.initializeSettingsApp(appContainer);
|
window.initializeSettingsApp(appContainer);
|
||||||
break;
|
break;
|
||||||
|
case 'wallet':
|
||||||
|
window.initializeMobileBankApp(appContainer);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return this.renderPlaceholderApp(currentApp);
|
return this.renderPlaceholderApp(currentApp);
|
||||||
}
|
}
|
||||||
@ -111,7 +114,8 @@ class App extends Component {
|
|||||||
mail: '',
|
mail: '',
|
||||||
icloud: '',
|
icloud: '',
|
||||||
photos: '',
|
photos: '',
|
||||||
safari: ''
|
safari: '',
|
||||||
|
wallet: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.createElement(
|
return this.createElement(
|
||||||
|
|||||||
436
arma/client/addons/phone/ui/_site/js/apps/wallet/index.js
Normal file
436
arma/client/addons/phone/ui/_site/js/apps/wallet/index.js
Normal file
@ -0,0 +1,436 @@
|
|||||||
|
/** @format */
|
||||||
|
|
||||||
|
let lastMobileBankRequest = 0;
|
||||||
|
let mobileBankNoticeTimer = null;
|
||||||
|
const MOBILE_BANK_REQUEST_COOLDOWN = 1000;
|
||||||
|
|
||||||
|
function defaultMobileBankState() {
|
||||||
|
return {
|
||||||
|
account: {
|
||||||
|
bank: 0,
|
||||||
|
cash: 0,
|
||||||
|
earnings: 0,
|
||||||
|
transactions: [],
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
creditLine: {
|
||||||
|
amountDue: 0,
|
||||||
|
approvedAmount: 0,
|
||||||
|
availableAmount: 0,
|
||||||
|
outstandingPrincipal: 0,
|
||||||
|
},
|
||||||
|
orgName: '',
|
||||||
|
playerName: '',
|
||||||
|
transferTargets: [],
|
||||||
|
uid: '',
|
||||||
|
},
|
||||||
|
notice: null,
|
||||||
|
pendingAction: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMobileBankState() {
|
||||||
|
return {
|
||||||
|
...defaultMobileBankState(),
|
||||||
|
...(globalState.getState().mobileBank || {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMobileBankState(patch) {
|
||||||
|
globalState.setState({
|
||||||
|
mobileBank: {
|
||||||
|
...getMobileBankState(),
|
||||||
|
...patch,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMobileBankCurrency(value) {
|
||||||
|
const amount = Math.floor(Number(value || 0));
|
||||||
|
return `$${Math.max(0, amount).toLocaleString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMobileBankAmount(value) {
|
||||||
|
const amount = Math.floor(Number(value || 0));
|
||||||
|
return Number.isFinite(amount) ? amount : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMobileBankEvent(event, data = {}) {
|
||||||
|
if (typeof A3API !== 'undefined' && A3API.SendAlert) {
|
||||||
|
A3API.SendAlert(JSON.stringify({ event, data }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
showMobileBankNotice('error', 'Bank bridge is unavailable.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestMobileBankRefresh(force = false) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (!force && now - lastMobileBankRequest < MOBILE_BANK_REQUEST_COOLDOWN) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMobileBankRequest = now;
|
||||||
|
return sendMobileBankEvent('phone::bank::refresh', {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestMobileBankTransfer(target, amountValue) {
|
||||||
|
const targetUid = String(target || '').trim();
|
||||||
|
const amount = normalizeMobileBankAmount(amountValue);
|
||||||
|
|
||||||
|
if (!targetUid) {
|
||||||
|
showMobileBankNotice('error', 'Choose a recipient.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount <= 0) {
|
||||||
|
showMobileBankNotice('error', 'Enter a valid transfer amount.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMobileBankState({ pendingAction: 'transfer' });
|
||||||
|
const sent = sendMobileBankEvent('phone::bank::transfer::request', {
|
||||||
|
amount,
|
||||||
|
from: 'bank',
|
||||||
|
target: targetUid,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sent) {
|
||||||
|
setMobileBankState({ pendingAction: '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestMobileBankDepositEarnings() {
|
||||||
|
const state = getMobileBankState();
|
||||||
|
const availableEarnings = normalizeMobileBankAmount(state.account.earnings);
|
||||||
|
|
||||||
|
if (availableEarnings <= 0) {
|
||||||
|
showMobileBankNotice('error', 'No earnings are available to deposit.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMobileBankState({ pendingAction: 'depositearnings' });
|
||||||
|
const sent = sendMobileBankEvent('phone::bank::depositEarnings::request', {
|
||||||
|
amount: availableEarnings,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sent) {
|
||||||
|
setMobileBankState({ pendingAction: '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestMobileBankRepayCreditLine(amountValue) {
|
||||||
|
const amount = normalizeMobileBankAmount(amountValue);
|
||||||
|
const state = getMobileBankState();
|
||||||
|
const amountDue = normalizeMobileBankAmount(state.session.creditLine?.amountDue);
|
||||||
|
|
||||||
|
if (amountDue <= 0) {
|
||||||
|
showMobileBankNotice('error', 'No credit line payment is due.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount <= 0) {
|
||||||
|
showMobileBankNotice('error', 'Enter a valid payment amount.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMobileBankState({ pendingAction: 'repaycreditline' });
|
||||||
|
const sent = sendMobileBankEvent('phone::bank::repayCreditLine::request', {
|
||||||
|
amount: Math.min(amount, amountDue),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sent) {
|
||||||
|
setMobileBankState({ pendingAction: '' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMobileBank(payload) {
|
||||||
|
const current = getMobileBankState();
|
||||||
|
setMobileBankState({
|
||||||
|
account: {
|
||||||
|
...current.account,
|
||||||
|
...(payload && payload.account ? payload.account : {}),
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
...current.session,
|
||||||
|
...(payload && payload.session ? payload.session : {}),
|
||||||
|
},
|
||||||
|
pendingAction: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMobileBankAccount(accountPatch) {
|
||||||
|
const current = getMobileBankState();
|
||||||
|
setMobileBankState({
|
||||||
|
account: {
|
||||||
|
...current.account,
|
||||||
|
...(accountPatch || {}),
|
||||||
|
},
|
||||||
|
pendingAction: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMobileBankNotice(type, message) {
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
setMobileBankState({
|
||||||
|
notice: {
|
||||||
|
type: type || 'info',
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
pendingAction: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mobileBankNoticeTimer) {
|
||||||
|
clearTimeout(mobileBankNoticeTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
mobileBankNoticeTimer = setTimeout(() => {
|
||||||
|
setMobileBankState({ notice: null });
|
||||||
|
mobileBankNoticeTimer = null;
|
||||||
|
}, 3200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mobileBankTransactionRows(transactions) {
|
||||||
|
const rows = Array.isArray(transactions) ? transactions.slice(0, 5) : [];
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
const empty = document.createElement('div');
|
||||||
|
empty.className = 'wallet-empty-state';
|
||||||
|
empty.textContent = 'No recent transactions';
|
||||||
|
return empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = document.createElement('div');
|
||||||
|
list.className = 'wallet-transaction-list';
|
||||||
|
|
||||||
|
rows.forEach((entry) => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'wallet-transaction-row';
|
||||||
|
|
||||||
|
const copy = document.createElement('div');
|
||||||
|
copy.className = 'wallet-transaction-copy';
|
||||||
|
|
||||||
|
const title = document.createElement('span');
|
||||||
|
title.className = 'wallet-transaction-title';
|
||||||
|
title.textContent = entry.type || 'Transaction';
|
||||||
|
|
||||||
|
const meta = document.createElement('span');
|
||||||
|
meta.className = 'wallet-transaction-meta';
|
||||||
|
meta.textContent = entry.date || 'Pending timestamp';
|
||||||
|
|
||||||
|
const value = document.createElement('span');
|
||||||
|
value.className = 'wallet-transaction-value';
|
||||||
|
value.textContent = formatMobileBankCurrency(entry.amount || 0);
|
||||||
|
|
||||||
|
copy.append(title, meta);
|
||||||
|
row.append(copy, value);
|
||||||
|
list.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeMobileBankApp(container) {
|
||||||
|
const state = getMobileBankState();
|
||||||
|
const { account, session, notice, pendingAction } = state;
|
||||||
|
const transferTargets = Array.isArray(session.transferTargets)
|
||||||
|
? session.transferTargets
|
||||||
|
: [];
|
||||||
|
const creditLine = session.creditLine || {};
|
||||||
|
const amountDue = normalizeMobileBankAmount(creditLine.amountDue);
|
||||||
|
const outstandingPrincipal = normalizeMobileBankAmount(creditLine.outstandingPrincipal);
|
||||||
|
|
||||||
|
requestMobileBankRefresh(false);
|
||||||
|
|
||||||
|
const appContainer = document.createElement('div');
|
||||||
|
appContainer.className = 'app-container wallet-app';
|
||||||
|
appContainer.setAttribute('role', 'main');
|
||||||
|
appContainer.setAttribute('aria-label', 'Wallet');
|
||||||
|
|
||||||
|
const navBar = new NavigationBar({
|
||||||
|
title: 'Wallet',
|
||||||
|
rightButton: {
|
||||||
|
element: 'button',
|
||||||
|
props: {
|
||||||
|
className: 'wallet-nav-button',
|
||||||
|
type: 'button',
|
||||||
|
disabled: pendingAction !== '',
|
||||||
|
onClick: () => requestMobileBankRefresh(true),
|
||||||
|
'aria-label': 'Refresh wallet',
|
||||||
|
},
|
||||||
|
content: 'Refresh',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
navBar.mount(appContainer);
|
||||||
|
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.className = 'content wallet-content';
|
||||||
|
|
||||||
|
if (notice && notice.message) {
|
||||||
|
const noticeElement = document.createElement('div');
|
||||||
|
noticeElement.className = `wallet-notice wallet-notice-${notice.type || 'info'}`;
|
||||||
|
noticeElement.textContent = notice.message;
|
||||||
|
content.appendChild(noticeElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hero = document.createElement('section');
|
||||||
|
hero.className = 'wallet-balance-card';
|
||||||
|
hero.innerHTML = `
|
||||||
|
<span class="wallet-eyebrow">Available Balance</span>
|
||||||
|
<strong class="wallet-balance">${formatMobileBankCurrency(account.bank)}</strong>
|
||||||
|
<span class="wallet-owner">${session.playerName || 'Personal account'}</span>
|
||||||
|
`;
|
||||||
|
content.appendChild(hero);
|
||||||
|
|
||||||
|
const metrics = document.createElement('section');
|
||||||
|
metrics.className = 'wallet-metrics';
|
||||||
|
metrics.innerHTML = `
|
||||||
|
<div class="wallet-metric">
|
||||||
|
<span>Cash</span>
|
||||||
|
<strong>${formatMobileBankCurrency(account.cash)}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="wallet-metric">
|
||||||
|
<span>Earnings</span>
|
||||||
|
<strong>${formatMobileBankCurrency(account.earnings)}</strong>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
content.appendChild(metrics);
|
||||||
|
|
||||||
|
const bankingActions = document.createElement('section');
|
||||||
|
bankingActions.className = 'wallet-card';
|
||||||
|
|
||||||
|
const bankingTitle = document.createElement('div');
|
||||||
|
bankingTitle.className = 'wallet-card-title';
|
||||||
|
bankingTitle.textContent = 'Account Actions';
|
||||||
|
|
||||||
|
const earningsAction = document.createElement('div');
|
||||||
|
earningsAction.className = 'wallet-action-block';
|
||||||
|
|
||||||
|
const earningsSummary = document.createElement('div');
|
||||||
|
earningsSummary.className = 'wallet-action-summary';
|
||||||
|
earningsSummary.innerHTML = `
|
||||||
|
<span>Deposit Earnings</span>
|
||||||
|
<strong>${formatMobileBankCurrency(account.earnings)} available</strong>
|
||||||
|
<small>Move mission earnings into your bank balance.</small>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const earningsButton = document.createElement('button');
|
||||||
|
earningsButton.className = 'wallet-secondary-button wallet-full-button';
|
||||||
|
earningsButton.type = 'button';
|
||||||
|
earningsButton.disabled = pendingAction !== '' || normalizeMobileBankAmount(account.earnings) <= 0;
|
||||||
|
earningsButton.textContent = pendingAction === 'depositearnings' ? 'Depositing...' : 'Deposit Earnings';
|
||||||
|
earningsButton.addEventListener('click', () => {
|
||||||
|
requestMobileBankDepositEarnings();
|
||||||
|
});
|
||||||
|
earningsAction.append(earningsSummary, earningsButton);
|
||||||
|
|
||||||
|
const creditAction = document.createElement('div');
|
||||||
|
creditAction.className = 'wallet-action-block';
|
||||||
|
|
||||||
|
const creditSummary = document.createElement('div');
|
||||||
|
creditSummary.className = 'wallet-action-summary';
|
||||||
|
creditSummary.innerHTML = `
|
||||||
|
<span>Credit Line Payment</span>
|
||||||
|
<strong>${formatMobileBankCurrency(amountDue)} due</strong>
|
||||||
|
<small>${session.orgName || 'Organization'} - ${formatMobileBankCurrency(outstandingPrincipal)} outstanding</small>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const creditControls = document.createElement('div');
|
||||||
|
creditControls.className = 'wallet-action-controls';
|
||||||
|
|
||||||
|
const creditAmount = document.createElement('input');
|
||||||
|
creditAmount.className = 'wallet-input';
|
||||||
|
creditAmount.type = 'number';
|
||||||
|
creditAmount.min = '1';
|
||||||
|
creditAmount.step = '1';
|
||||||
|
creditAmount.placeholder = amountDue > 0 ? 'Payment amount' : 'No payment due';
|
||||||
|
creditAmount.setAttribute('aria-label', 'Credit line payment amount');
|
||||||
|
creditAmount.inputMode = 'numeric';
|
||||||
|
creditAmount.disabled = pendingAction !== '' || amountDue <= 0;
|
||||||
|
|
||||||
|
const creditButton = document.createElement('button');
|
||||||
|
creditButton.className = 'wallet-secondary-button';
|
||||||
|
creditButton.type = 'button';
|
||||||
|
creditButton.disabled = pendingAction !== '' || amountDue <= 0;
|
||||||
|
creditButton.textContent = pendingAction === 'repaycreditline' ? 'Paying...' : 'Pay Credit';
|
||||||
|
creditButton.addEventListener('click', () => {
|
||||||
|
requestMobileBankRepayCreditLine(creditAmount.value || amountDue);
|
||||||
|
});
|
||||||
|
|
||||||
|
creditControls.append(creditAmount, creditButton);
|
||||||
|
creditAction.append(creditSummary, creditControls);
|
||||||
|
bankingActions.append(bankingTitle, earningsAction, creditAction);
|
||||||
|
content.appendChild(bankingActions);
|
||||||
|
|
||||||
|
const transferCard = document.createElement('section');
|
||||||
|
transferCard.className = 'wallet-card';
|
||||||
|
|
||||||
|
const transferTitle = document.createElement('div');
|
||||||
|
transferTitle.className = 'wallet-card-title';
|
||||||
|
transferTitle.textContent = 'Transfer';
|
||||||
|
|
||||||
|
const targetSelect = document.createElement('select');
|
||||||
|
targetSelect.className = 'wallet-input';
|
||||||
|
targetSelect.setAttribute('aria-label', 'Transfer recipient');
|
||||||
|
targetSelect.disabled = pendingAction !== '' || transferTargets.length === 0;
|
||||||
|
|
||||||
|
const placeholder = document.createElement('option');
|
||||||
|
placeholder.value = '';
|
||||||
|
placeholder.textContent = transferTargets.length === 0 ? 'No online recipients' : 'Choose recipient';
|
||||||
|
targetSelect.appendChild(placeholder);
|
||||||
|
|
||||||
|
transferTargets.forEach((target) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = target.uid || '';
|
||||||
|
option.textContent = target.name || target.uid || 'Player';
|
||||||
|
targetSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
const amountInput = document.createElement('input');
|
||||||
|
amountInput.className = 'wallet-input';
|
||||||
|
amountInput.type = 'number';
|
||||||
|
amountInput.min = '1';
|
||||||
|
amountInput.step = '1';
|
||||||
|
amountInput.placeholder = 'Amount';
|
||||||
|
amountInput.inputMode = 'numeric';
|
||||||
|
amountInput.disabled = pendingAction !== '';
|
||||||
|
|
||||||
|
const transferButton = document.createElement('button');
|
||||||
|
transferButton.className = 'wallet-primary-button';
|
||||||
|
transferButton.type = 'button';
|
||||||
|
transferButton.disabled = pendingAction !== '' || transferTargets.length === 0;
|
||||||
|
transferButton.textContent = pendingAction === 'transfer' ? 'Sending...' : 'Send Transfer';
|
||||||
|
transferButton.addEventListener('click', () => {
|
||||||
|
requestMobileBankTransfer(targetSelect.value, amountInput.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
transferCard.append(transferTitle, targetSelect, amountInput, transferButton);
|
||||||
|
content.appendChild(transferCard);
|
||||||
|
|
||||||
|
const historyCard = document.createElement('section');
|
||||||
|
historyCard.className = 'wallet-card';
|
||||||
|
|
||||||
|
const historyTitle = document.createElement('div');
|
||||||
|
historyTitle.className = 'wallet-card-title';
|
||||||
|
historyTitle.textContent = 'Recent Activity';
|
||||||
|
|
||||||
|
historyCard.append(historyTitle, mobileBankTransactionRows(account.transactions));
|
||||||
|
content.appendChild(historyCard);
|
||||||
|
|
||||||
|
appContainer.appendChild(content);
|
||||||
|
container.appendChild(appContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.initializeMobileBankApp = initializeMobileBankApp;
|
||||||
|
window.requestMobileBankRefresh = requestMobileBankRefresh;
|
||||||
|
window.updateMobileBank = updateMobileBank;
|
||||||
|
window.updateMobileBankAccount = updateMobileBankAccount;
|
||||||
|
window.showMobileBankNotice = showMobileBankNotice;
|
||||||
@ -94,6 +94,7 @@ class HomeScreen extends Component {
|
|||||||
{ name: 'photos', title: 'Photos', icon: 'Photos', color: '' },
|
{ name: 'photos', title: 'Photos', icon: 'Photos', color: '' },
|
||||||
{ name: 'clock', title: 'Clock', icon: 'Clock', color: '' },
|
{ name: 'clock', title: 'Clock', icon: 'Clock', color: '' },
|
||||||
{ name: 'calendar', title: 'Calendar', icon: 'Calendar', color: '' },
|
{ name: 'calendar', title: 'Calendar', icon: 'Calendar', color: '' },
|
||||||
|
{ name: 'wallet', title: 'Wallet', icon: 'Wallet', color: '' },
|
||||||
{ name: 'store', title: 'App Store', icon: 'AppStore', color: '' },
|
{ name: 'store', title: 'App Store', icon: 'AppStore', color: '' },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,6 +49,23 @@ const initialAppState = {
|
|||||||
events: [],
|
events: [],
|
||||||
currentEvent: null,
|
currentEvent: null,
|
||||||
showEventEditor: false,
|
showEventEditor: false,
|
||||||
|
|
||||||
|
// Mobile bank state
|
||||||
|
mobileBank: {
|
||||||
|
account: {
|
||||||
|
bank: 0,
|
||||||
|
cash: 0,
|
||||||
|
earnings: 0,
|
||||||
|
transactions: [],
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
playerName: '',
|
||||||
|
transferTargets: [],
|
||||||
|
uid: '',
|
||||||
|
},
|
||||||
|
notice: null,
|
||||||
|
pendingAction: '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
314
arma/client/addons/phone/ui/_site/styles/components/wallet.css
Normal file
314
arma/client/addons/phone/ui/_site/styles/components/wallet.css
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
/* Wallet */
|
||||||
|
.wallet-app {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-content {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
height: calc(100% - 44px);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px 16px 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-nav-button {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: #275a8c;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .wallet-nav-button {
|
||||||
|
color: #8bb9e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-notice {
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.35;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-notice-success {
|
||||||
|
background: rgba(47, 125, 91, 0.16);
|
||||||
|
color: #2f7d5b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-notice-error {
|
||||||
|
background: rgba(196, 57, 57, 0.16);
|
||||||
|
color: #b42323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-notice-info {
|
||||||
|
background: rgba(39, 90, 140, 0.14);
|
||||||
|
color: #275a8c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-balance-card {
|
||||||
|
background: linear-gradient(160deg, #142f52 0%, #275a8c 58%, #4f86bd 100%);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 14px 26px rgba(20, 47, 82, 0.22);
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 142px;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-eyebrow,
|
||||||
|
.wallet-card-title,
|
||||||
|
.wallet-metric span,
|
||||||
|
.wallet-transaction-meta {
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-eyebrow {
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-balance {
|
||||||
|
font-size: 34px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-owner {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-metrics {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-metric,
|
||||||
|
.wallet-card {
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #f4f8fd 100%);
|
||||||
|
border: 1px solid rgba(18, 54, 93, 0.12);
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .wallet-metric,
|
||||||
|
[data-theme="dark"] .wallet-card {
|
||||||
|
background: linear-gradient(180deg, #1c1c1e 0%, #151b23 100%);
|
||||||
|
border-color: rgba(139, 185, 230, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-metric {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-metric span,
|
||||||
|
.wallet-card-title {
|
||||||
|
color: #6f86a3;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .wallet-metric span,
|
||||||
|
[data-theme="dark"] .wallet-card-title,
|
||||||
|
[data-theme="dark"] .wallet-transaction-meta {
|
||||||
|
color: #8ea2bb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-metric strong {
|
||||||
|
color: #142f52;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .wallet-metric strong {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-input {
|
||||||
|
background: var(--input-bg);
|
||||||
|
border: 1px solid rgba(18, 54, 93, 0.16);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font: inherit;
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 0 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .wallet-input {
|
||||||
|
border-color: var(--input-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-primary-button {
|
||||||
|
background: #275a8c;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 800;
|
||||||
|
min-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-secondary-button {
|
||||||
|
background: rgba(39, 90, 140, 0.12);
|
||||||
|
border: 1px solid rgba(39, 90, 140, 0.18);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: #275a8c;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-full-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .wallet-secondary-button {
|
||||||
|
background: rgba(139, 185, 230, 0.13);
|
||||||
|
border-color: rgba(139, 185, 230, 0.2);
|
||||||
|
color: #8bb9e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-action-block {
|
||||||
|
background: rgba(39, 90, 140, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-action-block + .wallet-action-block {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .wallet-action-block {
|
||||||
|
background: rgba(139, 185, 230, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-action-summary {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-action-summary span {
|
||||||
|
color: #6f86a3;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-action-summary strong {
|
||||||
|
color: #142f52;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-action-summary small {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .wallet-action-summary span,
|
||||||
|
[data-theme="dark"] .wallet-action-summary strong {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-action-controls {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 112px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-primary-button:disabled,
|
||||||
|
.wallet-secondary-button:disabled,
|
||||||
|
.wallet-nav-button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.58;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-transaction-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-transaction-row {
|
||||||
|
align-items: center;
|
||||||
|
border-top: 1px solid rgba(18, 54, 93, 0.1);
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .wallet-transaction-row {
|
||||||
|
border-top-color: rgba(139, 185, 230, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-transaction-copy {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-transaction-title {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-transaction-meta {
|
||||||
|
color: #6f86a3;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-transaction-value {
|
||||||
|
color: #142f52;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .wallet-transaction-value {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wallet-empty-state {
|
||||||
|
border: 1px dashed rgba(18, 54, 93, 0.18);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@ -18,9 +18,12 @@ const files = [
|
|||||||
'../styles/components/contacts.css',
|
'../styles/components/contacts.css',
|
||||||
'../styles/components/dialpad.css',
|
'../styles/components/dialpad.css',
|
||||||
'../styles/components/messages.css',
|
'../styles/components/messages.css',
|
||||||
|
'../styles/components/mail.css',
|
||||||
'../styles/components/settings.css',
|
'../styles/components/settings.css',
|
||||||
'../styles/components/notes.css',
|
'../styles/components/notes.css',
|
||||||
|
'../styles/components/calendar.css',
|
||||||
'../styles/components/clock.css',
|
'../styles/components/clock.css',
|
||||||
|
'../styles/components/wallet.css',
|
||||||
'../styles/components/loader.css'
|
'../styles/components/loader.css'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,7 @@ const files = [
|
|||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
'../js/utils/helpers.js',
|
'../js/utils/helpers.js',
|
||||||
|
'../js/utils/PhoneMedia.js',
|
||||||
|
|
||||||
// Shared Components
|
// Shared Components
|
||||||
'../js/components/StatusBar.js',
|
'../js/components/StatusBar.js',
|
||||||
@ -34,6 +35,12 @@ const files = [
|
|||||||
'../js/apps/messages/components/ConversationView.js',
|
'../js/apps/messages/components/ConversationView.js',
|
||||||
'../js/apps/messages/index.js',
|
'../js/apps/messages/index.js',
|
||||||
|
|
||||||
|
// Mail App
|
||||||
|
'../js/apps/mail/components/MailList.js',
|
||||||
|
'../js/apps/mail/components/MailDetail.js',
|
||||||
|
'../js/apps/mail/components/MailComposer.js',
|
||||||
|
'../js/apps/mail/index.js',
|
||||||
|
|
||||||
// Contacts App
|
// Contacts App
|
||||||
'../js/apps/contacts/components/ContactList.js',
|
'../js/apps/contacts/components/ContactList.js',
|
||||||
'../js/apps/contacts/components/ContactItem.js',
|
'../js/apps/contacts/components/ContactItem.js',
|
||||||
@ -56,6 +63,13 @@ const files = [
|
|||||||
'../js/apps/clock/components/AlarmClock.js',
|
'../js/apps/clock/components/AlarmClock.js',
|
||||||
'../js/apps/clock/index.js',
|
'../js/apps/clock/index.js',
|
||||||
|
|
||||||
|
// Calendar App
|
||||||
|
'../js/apps/calendar/components/Calendar.js',
|
||||||
|
'../js/apps/calendar/components/EventEditor.js',
|
||||||
|
'../js/apps/calendar/index.js',
|
||||||
|
|
||||||
|
// Wallet App
|
||||||
|
'../js/apps/wallet/index.js',
|
||||||
|
|
||||||
// Main App
|
// Main App
|
||||||
'../js/app.js',
|
'../js/app.js',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user