You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
459 lines
19 KiB
459 lines
19 KiB
|
1 month ago
|
/**
|
||
|
|
* ssid.js — SSID Configuration Page
|
||
|
|
*
|
||
|
|
* Manages WiFi SSID list and WiFi connection settings (static/DHCP).
|
||
|
|
* IP fields use shared octet-split component.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { state, bindIpGroup, bindRadio } from '../state.js';
|
||
|
|
import { escapeHtml } from '../utils.js';
|
||
|
|
import { isValidIP, isValidCountryCode, normalizeCountryCode } from '../validator.js';
|
||
|
|
import { renderIpOctets, setupIpOctets, collectIpValue } from '../components/ip-input.js';
|
||
|
|
import { DEFAULTS } from '../constants.js';
|
||
|
|
import { COUNTRY_CODES, CONTINENT_GROUPS, FREQUENT_COUNTRY_CODES } from '../country-codes.js';
|
||
|
|
|
||
|
|
const MAX_SSID_COUNT = 10;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Build the grouped <optgroup>/<option> markup for the Country Code <select>:
|
||
|
|
* a pinned "Frequently used" group above five continent groups (205 codes).
|
||
|
|
* `selected` is the current stored code; if it is not a recognized code (an
|
||
|
|
* empty value, or a stale "EU" from the old free-text field), the dropdown
|
||
|
|
* falls back to the AE default. The `selected` attribute is applied to the
|
||
|
|
* first occurrence only, so exactly one option is selected.
|
||
|
|
*/
|
||
|
|
function renderCountryOptions(selected) {
|
||
|
|
const want = (selected || DEFAULTS.WIFI_COUNTRY_CODE).toUpperCase();
|
||
|
|
const sel = COUNTRY_CODES[want] ? want : DEFAULTS.WIFI_COUNTRY_CODE;
|
||
|
|
let selPlaced = false;
|
||
|
|
|
||
|
|
const option = (code) => {
|
||
|
|
const isSel = !selPlaced && code === sel;
|
||
|
|
if (isSel) selPlaced = true;
|
||
|
|
return `<option value="${code}"${isSel ? ' selected' : ''}>` +
|
||
|
|
`${escapeHtml(code + ' — ' + COUNTRY_CODES[code])}</option>`;
|
||
|
|
};
|
||
|
|
const group = (label, codes) =>
|
||
|
|
`<optgroup label="${escapeHtml(label)}">` +
|
||
|
|
codes.map(option).join('') +
|
||
|
|
`</optgroup>`;
|
||
|
|
|
||
|
|
let html = group('Frequently used', FREQUENT_COUNTRY_CODES);
|
||
|
|
html += CONTINENT_GROUPS.map(g => group(g.region, g.codes)).join('');
|
||
|
|
return html;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function renderSsidPage(container) {
|
||
|
|
const device = state.device || {};
|
||
|
|
const wifi = device.wifi || {};
|
||
|
|
const ssidList = device.ssid_list || [];
|
||
|
|
|
||
|
|
container.innerHTML = `
|
||
|
|
<div class="page-header">
|
||
|
|
<h1 class="page-header__title">📶 WiFi SSID Management</h1>
|
||
|
|
<p class="page-header__desc">Manage WiFi network list and connection settings.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- SSID List -->
|
||
|
|
<div class="card">
|
||
|
|
<div class="card__header">
|
||
|
|
<h2 class="card__title"><span class="card__title-icon">📡</span> SSID List</h2>
|
||
|
|
<button class="btn btn--outline btn--sm" id="ssid-add-btn">+ Add</button>
|
||
|
|
</div>
|
||
|
|
<div class="data-table-wrapper">
|
||
|
|
<table class="data-table" id="ssid-table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>SSID Name</th>
|
||
|
|
<th>Password</th>
|
||
|
|
<th>Security</th>
|
||
|
|
<th class="col-action"></th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="ssid-tbody"></tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
<p class="form-hint mt-sm">Up to 10 SSIDs can be registered.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- WiFi Connection Settings -->
|
||
|
|
<div class="card">
|
||
|
|
<div class="card__header">
|
||
|
|
<h2 class="card__title"><span class="card__title-icon">🔧</span> WiFi Connection Settings</h2>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="form-group">
|
||
|
|
<label class="form-label">Connection Type</label>
|
||
|
|
<div class="radio-group">
|
||
|
|
<label class="radio-label">
|
||
|
|
<input type="radio" name="wifi_type" value="static" ${wifi.type !== 'dhcp' ? 'checked' : ''}>
|
||
|
|
Static
|
||
|
|
</label>
|
||
|
|
<label class="radio-label">
|
||
|
|
<input type="radio" name="wifi_type" value="dhcp" ${wifi.type === 'dhcp' ? 'checked' : ''}>
|
||
|
|
DHCP
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="wifi-static-fields">
|
||
|
|
<div class="form-row">
|
||
|
|
<div class="form-group">
|
||
|
|
<label class="form-label">IP Address</label>
|
||
|
|
${renderIpOctets('wifi_ip', wifi.ip)}
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label class="form-label">Subnet Mask</label>
|
||
|
|
${renderIpOctets('wifi_netmask', wifi.netmask)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="form-row">
|
||
|
|
<div class="form-group">
|
||
|
|
<label class="form-label">Gateway</label>
|
||
|
|
${renderIpOctets('wifi_gateway', wifi.gateway)}
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label class="form-label">DNS 1</label>
|
||
|
|
${renderIpOctets('wifi_dns1', wifi.dns1)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="form-row">
|
||
|
|
<div class="form-group">
|
||
|
|
<label class="form-label">DNS 2</label>
|
||
|
|
${renderIpOctets('wifi_dns2', wifi.dns2)}
|
||
|
|
</div>
|
||
|
|
<div class="form-group"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
|
||
|
|
// Setup IP octet event listeners
|
||
|
|
setupIpOctets(container);
|
||
|
|
|
||
|
|
// Populate SSID rows
|
||
|
|
const tbody = document.getElementById('ssid-tbody');
|
||
|
|
ssidList.forEach((s, i) => addSsidRow(tbody, s, i));
|
||
|
|
|
||
|
|
// Add SSID button (max 10 rows)
|
||
|
|
const addBtn = document.getElementById('ssid-add-btn');
|
||
|
|
addBtn.addEventListener('click', () => {
|
||
|
|
if (tbody.querySelectorAll('tr').length >= MAX_SSID_COUNT) return;
|
||
|
|
addSsidRow(tbody, { ssid: '', password: '', security: 'wpa/wpa2' }, ssidList.length);
|
||
|
|
updateSsidAddButton();
|
||
|
|
});
|
||
|
|
updateSsidAddButton();
|
||
|
|
|
||
|
|
// Static/DHCP toggle
|
||
|
|
document.querySelectorAll('input[name="wifi_type"]').forEach(radio => {
|
||
|
|
radio.addEventListener('change', () => toggleStaticFields());
|
||
|
|
});
|
||
|
|
toggleStaticFields();
|
||
|
|
|
||
|
|
// Register data collector
|
||
|
|
window.__pageCollectors.ssid = collectSsidData;
|
||
|
|
}
|
||
|
|
|
||
|
|
function addSsidRow(tbody, data, index) {
|
||
|
|
const tr = document.createElement('tr');
|
||
|
|
tr.innerHTML = `
|
||
|
|
<td><input class="form-input" data-field="ssid" value="${escapeHtml(data.ssid)}" placeholder="SSID Name"></td>
|
||
|
|
<td>
|
||
|
|
<div class="input-password">
|
||
|
|
<input class="form-input" type="password" data-field="password" value="${escapeHtml(data.password)}" placeholder="Password">
|
||
|
|
<button class="input-password__toggle" type="button" title="Show password">👁</button>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
<td>
|
||
|
|
<select class="form-select" data-field="security">
|
||
|
|
<option value="wpa/wpa2" ${data.security === 'wpa/wpa2' || data.security === 'WPA' ? 'selected' : ''}>WPA/WPA2</option>
|
||
|
|
<option value="WEP" ${data.security === 'WEP' ? 'selected' : ''}>WEP</option>
|
||
|
|
<option value="Open" ${data.security === 'Open' ? 'selected' : ''}>Open</option>
|
||
|
|
</select>
|
||
|
|
</td>
|
||
|
|
<td class="col-action">
|
||
|
|
<button class="btn btn--ghost btn--icon ssid-delete" title="Delete">🗑</button>
|
||
|
|
</td>
|
||
|
|
`;
|
||
|
|
|
||
|
|
// Toggle password visibility
|
||
|
|
tr.querySelector('.input-password__toggle').addEventListener('click', (e) => {
|
||
|
|
const input = tr.querySelector('input[data-field="password"]');
|
||
|
|
input.type = input.type === 'password' ? 'text' : 'password';
|
||
|
|
e.target.textContent = input.type === 'password' ? '👁' : '🔒';
|
||
|
|
});
|
||
|
|
|
||
|
|
// Delete row
|
||
|
|
tr.querySelector('.ssid-delete').addEventListener('click', () => {
|
||
|
|
tr.remove();
|
||
|
|
updateSsidAddButton();
|
||
|
|
});
|
||
|
|
|
||
|
|
tbody.appendChild(tr);
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateSsidAddButton() {
|
||
|
|
const btn = document.getElementById('ssid-add-btn');
|
||
|
|
const count = document.querySelectorAll('#ssid-tbody tr').length;
|
||
|
|
if (btn) btn.disabled = count >= MAX_SSID_COUNT;
|
||
|
|
}
|
||
|
|
|
||
|
|
function toggleStaticFields() {
|
||
|
|
const isDhcp = document.querySelector('input[name="wifi_type"]:checked')?.value === 'dhcp';
|
||
|
|
const fields = document.getElementById('wifi-static-fields');
|
||
|
|
if (!fields) return;
|
||
|
|
fields.querySelectorAll('.form-input').forEach(input => {
|
||
|
|
input.disabled = isDhcp;
|
||
|
|
});
|
||
|
|
fields.style.opacity = isDhcp ? '0.5' : '1';
|
||
|
|
}
|
||
|
|
|
||
|
|
function collectSsidData() {
|
||
|
|
if (!state.device) state.device = {};
|
||
|
|
|
||
|
|
// WiFi settings — collect from octet groups.
|
||
|
|
// country_code MUST be re-collected here: this object replaces state.device.wifi
|
||
|
|
// wholesale, so omitting it would silently drop the legacy Java value on save.
|
||
|
|
const wifiType = document.querySelector('input[name="wifi_type"]:checked')?.value || 'static';
|
||
|
|
const ccEl = document.getElementById('wifi_country_code');
|
||
|
|
const countryCode = normalizeCountryCode(
|
||
|
|
ccEl ? ccEl.value : (state.device.wifi && state.device.wifi.country_code)
|
||
|
|
) || DEFAULTS.WIFI_COUNTRY_CODE;
|
||
|
|
state.device.wifi = {
|
||
|
|
type: wifiType,
|
||
|
|
ip: collectIpValue('wifi_ip'),
|
||
|
|
netmask: collectIpValue('wifi_netmask'),
|
||
|
|
gateway: collectIpValue('wifi_gateway'),
|
||
|
|
dns1: collectIpValue('wifi_dns1'),
|
||
|
|
dns2: collectIpValue('wifi_dns2'),
|
||
|
|
country_code: countryCode,
|
||
|
|
};
|
||
|
|
|
||
|
|
// SSID list
|
||
|
|
const rows = document.querySelectorAll('#ssid-tbody tr');
|
||
|
|
state.device.ssid_list = Array.from(rows).map(tr => ({
|
||
|
|
ssid: tr.querySelector('[data-field="ssid"]')?.value || '',
|
||
|
|
password: tr.querySelector('[data-field="password"]')?.value || '',
|
||
|
|
security: tr.querySelector('[data-field="security"]')?.value || 'wpa/wpa2',
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── WiFi IP fields ─────────────────────────────────────────
|
||
|
|
const WIFI_IP_FIELDS = ['wifi_ip', 'wifi_netmask', 'wifi_gateway', 'wifi_dns1', 'wifi_dns2'];
|
||
|
|
const WIFI_IP_LABELS = {
|
||
|
|
wifi_ip: 'WiFi IP', wifi_netmask: 'WiFi Subnet Mask', wifi_gateway: 'WiFi Gateway',
|
||
|
|
wifi_dns1: 'WiFi DNS 1', wifi_dns2: 'WiFi DNS 2',
|
||
|
|
};
|
||
|
|
const WIFI_STATE_KEYS = { wifi_ip: 'ip', wifi_netmask: 'netmask', wifi_gateway: 'gateway', wifi_dns1: 'dns1', wifi_dns2: 'dns2' };
|
||
|
|
|
||
|
|
// ─── New Page Interface ─────────────────────────────────────
|
||
|
|
const ssidPage = {
|
||
|
|
render(container) {
|
||
|
|
const device = state.device || {};
|
||
|
|
const wifi = device.wifi || {};
|
||
|
|
const ssidList = device.ssid_list || [];
|
||
|
|
|
||
|
|
container.innerHTML = `
|
||
|
|
<div class="page-header">
|
||
|
|
<h1 class="page-header__title">📶 WiFi SSID Management</h1>
|
||
|
|
<p class="page-header__desc">Manage WiFi network list and connection settings.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="card">
|
||
|
|
<div class="card__header">
|
||
|
|
<h2 class="card__title"><span class="card__title-icon">📡</span> SSID List</h2>
|
||
|
|
<button class="btn btn--outline btn--sm" id="ssid-add-btn">+ Add</button>
|
||
|
|
</div>
|
||
|
|
<div class="data-table-wrapper">
|
||
|
|
<table class="data-table" id="ssid-table">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>SSID Name</th>
|
||
|
|
<th>Password</th>
|
||
|
|
<th>Security</th>
|
||
|
|
<th class="col-action"></th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="ssid-tbody"></tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
<p class="form-hint mt-sm">Up to 10 SSIDs can be registered.</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="card">
|
||
|
|
<div class="card__header">
|
||
|
|
<h2 class="card__title"><span class="card__title-icon">🔧</span> WiFi Connection Settings</h2>
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label class="form-label">Connection Type</label>
|
||
|
|
<div class="radio-group">
|
||
|
|
<label class="radio-label">
|
||
|
|
<input type="radio" name="wifi_type" value="static" ${wifi.type !== 'dhcp' ? 'checked' : ''}>
|
||
|
|
Static
|
||
|
|
</label>
|
||
|
|
<label class="radio-label">
|
||
|
|
<input type="radio" name="wifi_type" value="dhcp" ${wifi.type === 'dhcp' ? 'checked' : ''}>
|
||
|
|
DHCP
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div id="wifi-static-fields">
|
||
|
|
<div class="form-row">
|
||
|
|
<div class="form-group">
|
||
|
|
<label class="form-label">IP Address</label>
|
||
|
|
${renderIpOctets('wifi_ip', wifi.ip)}
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label class="form-label">Subnet Mask</label>
|
||
|
|
${renderIpOctets('wifi_netmask', wifi.netmask)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="form-row">
|
||
|
|
<div class="form-group">
|
||
|
|
<label class="form-label">Gateway</label>
|
||
|
|
${renderIpOctets('wifi_gateway', wifi.gateway)}
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label class="form-label">DNS 1</label>
|
||
|
|
${renderIpOctets('wifi_dns1', wifi.dns1)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="form-row">
|
||
|
|
<div class="form-group">
|
||
|
|
<label class="form-label">DNS 2</label>
|
||
|
|
${renderIpOctets('wifi_dns2', wifi.dns2)}
|
||
|
|
</div>
|
||
|
|
<div class="form-group"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="card">
|
||
|
|
<div class="card__header">
|
||
|
|
<h2 class="card__title"><span class="card__title-icon">🌍</span> Region</h2>
|
||
|
|
</div>
|
||
|
|
<div class="form-group">
|
||
|
|
<label class="form-label" for="wifi_country_code">Country Code</label>
|
||
|
|
<select class="form-input" id="wifi_country_code" style="max-width: 300px;">
|
||
|
|
${renderCountryOptions(wifi.country_code || DEFAULTS.WIFI_COUNTRY_CODE)}
|
||
|
|
</select>
|
||
|
|
<p class="form-hint">WiFi regulatory region. Defaults to AE (United Arab Emirates); dpworldapp applies it to the WiFi module.</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
},
|
||
|
|
|
||
|
|
mount(container) {
|
||
|
|
setupIpOctets(container);
|
||
|
|
|
||
|
|
// Reactive bindings: WiFi type radio
|
||
|
|
bindRadio(container, 'wifi_type',
|
||
|
|
() => (state.device?.wifi?.type || 'static'),
|
||
|
|
(val) => {
|
||
|
|
if (!state.device) state.device = {};
|
||
|
|
if (!state.device.wifi) state.device.wifi = {};
|
||
|
|
state.device.wifi.type = val;
|
||
|
|
state.isDirty = true;
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
// Reactive bindings: WiFi IP groups
|
||
|
|
WIFI_IP_FIELDS.forEach(ipKey => {
|
||
|
|
const stateKey = WIFI_STATE_KEYS[ipKey];
|
||
|
|
bindIpGroup(container, ipKey,
|
||
|
|
() => state.device?.wifi?.[stateKey] || '',
|
||
|
|
(val) => {
|
||
|
|
if (!state.device) state.device = {};
|
||
|
|
if (!state.device.wifi) state.device.wifi = {};
|
||
|
|
state.device.wifi[stateKey] = val;
|
||
|
|
state.isDirty = true;
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Reactive binding: WiFi country code (<select>)
|
||
|
|
const ccEl = container.querySelector('#wifi_country_code');
|
||
|
|
if (ccEl) {
|
||
|
|
ccEl.addEventListener('change', () => {
|
||
|
|
if (!state.device) state.device = {};
|
||
|
|
if (!state.device.wifi) state.device.wifi = {};
|
||
|
|
state.device.wifi.country_code = ccEl.value;
|
||
|
|
state.isDirty = true;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// SSID list: populate + event delegation
|
||
|
|
const tbody = container.querySelector('#ssid-tbody');
|
||
|
|
const ssidList = state.device?.ssid_list || [];
|
||
|
|
ssidList.forEach((s, i) => addSsidRow(tbody, s, i));
|
||
|
|
|
||
|
|
// SSID list: reactive sync on input/change/delete
|
||
|
|
tbody.addEventListener('input', () => { this._syncSsidList(tbody); });
|
||
|
|
tbody.addEventListener('change', () => { this._syncSsidList(tbody); });
|
||
|
|
// Sync after row deletion (DOM mutation via click on delete button)
|
||
|
|
tbody.addEventListener('click', (e) => {
|
||
|
|
if (e.target.closest('.ssid-delete')) {
|
||
|
|
// Delay to let the row be removed first
|
||
|
|
setTimeout(() => { this._syncSsidList(tbody); updateSsidAddButton(); }, 0);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Add button
|
||
|
|
const addBtn = container.querySelector('#ssid-add-btn');
|
||
|
|
addBtn.addEventListener('click', () => {
|
||
|
|
if (tbody.querySelectorAll('tr').length >= MAX_SSID_COUNT) return;
|
||
|
|
addSsidRow(tbody, { ssid: '', password: '', security: 'wpa/wpa2' }, tbody.querySelectorAll('tr').length);
|
||
|
|
this._syncSsidList(tbody);
|
||
|
|
updateSsidAddButton();
|
||
|
|
});
|
||
|
|
updateSsidAddButton();
|
||
|
|
|
||
|
|
// Static/DHCP toggle
|
||
|
|
container.querySelectorAll('input[name="wifi_type"]').forEach(radio => {
|
||
|
|
radio.addEventListener('change', () => toggleStaticFields());
|
||
|
|
});
|
||
|
|
toggleStaticFields();
|
||
|
|
|
||
|
|
// Legacy collector (R4)
|
||
|
|
window.__pageCollectors.ssid = collectSsidData;
|
||
|
|
},
|
||
|
|
|
||
|
|
_syncSsidList(tbody) {
|
||
|
|
if (!state.device) state.device = {};
|
||
|
|
const rows = tbody.querySelectorAll('tr');
|
||
|
|
state.device.ssid_list = Array.from(rows).map(tr => ({
|
||
|
|
ssid: tr.querySelector('[data-field="ssid"]')?.value || '',
|
||
|
|
password: tr.querySelector('[data-field="password"]')?.value || '',
|
||
|
|
security: tr.querySelector('[data-field="security"]')?.value || 'wpa/wpa2',
|
||
|
|
}));
|
||
|
|
state.isDirty = true;
|
||
|
|
},
|
||
|
|
|
||
|
|
destroy() {},
|
||
|
|
|
||
|
|
validate() {
|
||
|
|
const errors = [];
|
||
|
|
const wifi = state.device?.wifi || {};
|
||
|
|
// Only validate IP fields when static mode
|
||
|
|
if (wifi.type !== 'dhcp') {
|
||
|
|
WIFI_IP_FIELDS.forEach(ipKey => {
|
||
|
|
const stateKey = WIFI_STATE_KEYS[ipKey];
|
||
|
|
const val = wifi[stateKey];
|
||
|
|
if (val && val !== '...' && !isValidIP(val)) {
|
||
|
|
errors.push({ field: ipKey, page: 'ssid', message: `${WIFI_IP_LABELS[ipKey]} — Invalid IP format` });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
// Region — WiFi country code: required, exactly 2 uppercase letters
|
||
|
|
if (!isValidCountryCode(normalizeCountryCode(wifi.country_code))) {
|
||
|
|
errors.push({
|
||
|
|
field: 'wifi_country_code', page: 'ssid',
|
||
|
|
message: 'Country Code — must be a 2-letter ISO 3166-1 alpha-2 code (e.g. KR, US, DE, JP, AE)',
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return errors;
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
export default ssidPage;
|