apply review comments

This commit is contained in:
eglehelms
2022-12-07 15:17:20 +01:00
parent c54595c99c
commit bb460c82ef
14 changed files with 431 additions and 287 deletions
+3
View File
@@ -106,7 +106,9 @@ export interface User {
is_active: boolean;
force_change: boolean;
account_sid: string | null;
account_name?: string | null;
service_provider_sid: string | null;
service_provider_name?: string | null;
initial_password?: string;
permissions?: UserPermissions[];
}
@@ -142,6 +144,7 @@ export interface UserJWT {
account_sid?: string | null;
service_provider_sid?: string | null;
permissions: UserPermissions[];
name: string;
}
export interface ServiceProvider {
+2
View File
@@ -5,6 +5,7 @@ import {
Menu,
List,
Edit,
User,
Plus,
Grid,
Phone,
@@ -53,6 +54,7 @@ export const Icons: IconMap = {
Menu,
List,
Edit,
User,
Plus,
Grid,
Phone,
+23 -2
View File
@@ -1,17 +1,22 @@
import React, { useState } from "react";
import { Outlet } from "react-router-dom";
import { Link, Outlet } from "react-router-dom";
import { Button, Icon, classNames } from "jambonz-ui";
import { Navi } from "./navi";
import { Icons } from "src/components";
import { toastSuccess } from "src/store";
import { toastSuccess, useSelectState } from "src/store";
import { useAuth } from "src/router/auth";
import { useMobileMedia } from "src/utils";
import { MSG_LOGGED_OUT } from "src/constants";
import "./styles.scss";
import { ROUTE_INTERNAL_USERS } from "src/router/routes";
import { useApiData } from "src/api";
import { User } from "src/api/types";
export const Layout = () => {
const user = useSelectState("user");
const [userData] = useApiData<User>("Users/me");
const [active, setActive] = useState(false);
const { signout } = useAuth();
const mobile = useMobileMedia();
@@ -43,6 +48,22 @@ export const Layout = () => {
<Icon subStyle="dark" onClick={handleMenu}>
<Icons.Menu />
</Icon>
<div className="user">
<Icons.User className="user--icon" />
<div className="item__info">
<div className="user--txt">
<Link
to={`${ROUTE_INTERNAL_USERS}/${user?.user_sid}/edit`}
title="Edit user"
>
<strong>{userData?.name}</strong>
</Link>
</div>
<div>
<strong>Scope:</strong> <code>{user?.scope}</code>
</div>
</div>
</div>
<Button
small
mainStyle="hollow"
@@ -114,7 +114,7 @@ export const Alerts = () => {
</div>
))
) : (
<M>No data</M>
<M>No data.</M>
)}
</div>
</Section>
+320 -267
View File
@@ -36,6 +36,7 @@ import {
AccountSelect,
ApplicationSelect,
} from "src/components/forms";
import { ScopedAccess } from "src/components/scoped-access";
import { MSG_REQUIRED_FIELDS } from "src/constants";
import { ROUTE_INTERNAL_CARRIERS } from "src/router/routes";
import { toastError, toastSuccess, useSelectState } from "src/store";
@@ -52,6 +53,7 @@ import type {
Smpp,
Application,
} from "src/api/types";
import { Scope } from "src/store/types";
type CarrierFormProps = {
carrier?: UseApiDataMap<Carrier>;
@@ -746,117 +748,122 @@ export const CarrierForm = ({
</>
)}
</fieldset>
<fieldset>
<Checkzone
hidden
name="sip_credentials"
label="Outbound Authentication"
initialCheck={initialRegister}
handleChecked={(e) => {
if (!e.target.checked) {
setSipUser("");
setSipPass("");
setSipRealm("");
setSipRegister(false);
setFromUser("");
setFromDomain("");
setRegPublicIpInContact(false);
}
}}
>
<MS>
Does your carrier require authentication on outbound calls?
</MS>
<label htmlFor="sip_username">
Username {sipPass || sipRegister ? <span>*</span> : ""}
</label>
<input
id="sip_username"
name="sip_username"
type="text"
value={sipUser}
placeholder="SIP username for authenticating outbound calls"
required={sipRegister || sipPass.length > 0}
onChange={(e) => {
setSipUser(e.target.value);
<ScopedAccess
user={user}
scope={accountSid ? Scope.account : Scope.service_provider}
>
<fieldset>
<Checkzone
hidden
name="sip_credentials"
label="Outbound Authentication"
initialCheck={initialRegister}
handleChecked={(e) => {
if (!e.target.checked) {
setSipUser("");
setSipPass("");
setSipRealm("");
setSipRegister(false);
setFromUser("");
setFromDomain("");
setRegPublicIpInContact(false);
}
}}
/>
<label htmlFor="sip_password">
Password
{sipUser || sipRegister ? <span>*</span> : ""}
</label>
<Passwd
id="sip_password"
name="sip_password"
value={sipPass}
placeholder="SIP password for authenticating outbound calls"
required={sipRegister || sipUser.length > 0}
onChange={(e) => {
setSipPass(e.target.value);
}}
/>
<label htmlFor="sip_register" className="chk">
>
<MS>
Does your carrier require authentication on outbound calls?
</MS>
<label htmlFor="sip_username">
Username {sipPass || sipRegister ? <span>*</span> : ""}
</label>
<input
id="sip_register"
name="sip_register"
type="checkbox"
checked={sipRegister}
onChange={(e) => setSipRegister(e.target.checked)}
id="sip_username"
name="sip_username"
type="text"
value={sipUser}
placeholder="SIP username for authenticating outbound calls"
required={sipRegister || sipPass.length > 0}
onChange={(e) => {
setSipUser(e.target.value);
}}
/>
<div>Require SIP Register</div>
</label>
{sipRegister && (
<>
<MS>
Carrier requires SIP Register before sending outbound
calls.
</MS>
<label htmlFor="sip_realm">
SIP Realm{sipRegister ? <span>*</span> : ""}
</label>
<label htmlFor="sip_password">
Password
{sipUser || sipRegister ? <span>*</span> : ""}
</label>
<Passwd
id="sip_password"
name="sip_password"
value={sipPass}
placeholder="SIP password for authenticating outbound calls"
required={sipRegister || sipUser.length > 0}
onChange={(e) => {
setSipPass(e.target.value);
}}
/>
<label htmlFor="sip_register" className="chk">
<input
id="sip_realm"
name="sip_realm"
type="text"
value={sipRealm}
placeholder="SIP realm for registration"
required={sipRegister}
onChange={(e) => setSipRealm(e.target.value)}
id="sip_register"
name="sip_register"
type="checkbox"
checked={sipRegister}
onChange={(e) => setSipRegister(e.target.checked)}
/>
<label htmlFor="from_user">SIP From User</label>
<input
id="from_user"
name="from_user"
type="text"
value={fromUser}
placeholder="Optional: specify user part of SIP From header"
onChange={(e) => setFromUser(e.target.value)}
/>
<label htmlFor="from_domain">SIP From Domain</label>
<input
id="from_domain"
name="from_domain"
type="text"
value={fromDomain}
placeholder="Optional: specify host part of SIP From header"
onChange={(e) => setFromDomain(e.target.value)}
/>
<label htmlFor="reg_public_ip_in_contact" className="chk">
<div>Require SIP Register</div>
</label>
{sipRegister && (
<>
<MS>
Carrier requires SIP Register before sending outbound
calls.
</MS>
<label htmlFor="sip_realm">
SIP Realm{sipRegister ? <span>*</span> : ""}
</label>
<input
id="reg_public_ip_in_contact"
name="reg_public_ip_in_contact"
type="checkbox"
checked={regPublicIpInContact}
onChange={(e) =>
setRegPublicIpInContact(e.target.checked)
}
id="sip_realm"
name="sip_realm"
type="text"
value={sipRealm}
placeholder="SIP realm for registration"
required={sipRegister}
onChange={(e) => setSipRealm(e.target.value)}
/>
<div>Use Public IP in Contact</div>
</label>
</>
)}
</Checkzone>
</fieldset>
<label htmlFor="from_user">SIP From User</label>
<input
id="from_user"
name="from_user"
type="text"
value={fromUser}
placeholder="Optional: specify user part of SIP From header"
onChange={(e) => setFromUser(e.target.value)}
/>
<label htmlFor="from_domain">SIP From Domain</label>
<input
id="from_domain"
name="from_domain"
type="text"
value={fromDomain}
placeholder="Optional: specify host part of SIP From header"
onChange={(e) => setFromDomain(e.target.value)}
/>
<label htmlFor="reg_public_ip_in_contact" className="chk">
<input
id="reg_public_ip_in_contact"
name="reg_public_ip_in_contact"
type="checkbox"
checked={regPublicIpInContact}
onChange={(e) =>
setRegPublicIpInContact(e.target.checked)
}
/>
<div>Use Public IP in Contact</div>
</label>
</>
)}
</Checkzone>
</fieldset>
</ScopedAccess>
<fieldset>
<Checkzone
hidden
@@ -1021,49 +1028,61 @@ export const CarrierForm = ({
</label>
</div>
</div>
<button
className="btnty"
title="Delete SIP Gateway"
type="button"
onClick={() => {
setSipMessage("");
if (sipGateways.length === 1) {
setSipMessage(
"You must provide at least one SIP Gateway."
);
} else {
handleSipGatewayDelete(
sipGateways.find((g2, i2) => i2 === i)
);
setSipGateways(
sipGateways.filter((g2, i2) => i2 !== i)
);
}
}}
<ScopedAccess
user={user}
scope={
accountSid ? Scope.account : Scope.service_provider
}
>
<Icon>
<Icons.Trash2 />
</Icon>
</button>
<button
className="btnty"
title="Delete SIP Gateway"
type="button"
onClick={() => {
setSipMessage("");
if (sipGateways.length === 1) {
setSipMessage(
"You must provide at least one SIP Gateway."
);
} else {
handleSipGatewayDelete(
sipGateways.find((g2, i2) => i2 === i)
);
setSipGateways(
sipGateways.filter((g2, i2) => i2 !== i)
);
}
}}
>
<Icon>
<Icons.Trash2 />
</Icon>
</button>
</ScopedAccess>
</div>
))}
<ButtonGroup left>
<button
className="btnty"
type="button"
title="Add SIP Gateway"
onClick={() => {
setSipMessage("");
addSipGateway();
}}
>
<Icon subStyle="teal">
<Icons.Plus />
</Icon>
</button>
</ButtonGroup>
<ScopedAccess
user={user}
scope={accountSid ? Scope.account : Scope.service_provider}
>
<ButtonGroup left>
<button
className="btnty"
type="button"
title="Add SIP Gateway"
onClick={() => {
setSipMessage("");
addSipGateway();
}}
>
<Icon subStyle="teal">
<Icons.Plus />
</Icon>
</button>
</ButtonGroup>
</ScopedAccess>
</fieldset>
</Tab>
<Tab id="smpp" label="SMS">
@@ -1085,27 +1104,32 @@ export const CarrierForm = ({
</fieldset>
<fieldset>
<label htmlFor="outbound_smpp">Outbound SMPP</label>
<label htmlFor="outbound_id">System ID</label>
<input
id="outbound_id"
name="outbound_id"
type="text"
value={smppSystemId}
placeholder="SMPP system ID to authenticate with"
onChange={(e) => {
setSmppSystemId(e.target.value);
}}
/>
<label htmlFor="outbound_pass">Password</label>
<Passwd
id="outbound_pass"
name="outbound_pass"
value={smppPass}
placeholder="SMPP password to authenticate with"
onChange={(e) => {
setSmppPass(e.target.value);
}}
/>
<ScopedAccess
user={user}
scope={accountSid ? Scope.account : Scope.service_provider}
>
<label htmlFor="outbound_id">System ID</label>
<input
id="outbound_id"
name="outbound_id"
type="text"
value={smppSystemId}
placeholder="SMPP system ID to authenticate with"
onChange={(e) => {
setSmppSystemId(e.target.value);
}}
/>
<label htmlFor="outbound_pass">Password</label>
<Passwd
id="outbound_pass"
name="outbound_pass"
value={smppPass}
placeholder="SMPP password to authenticate with"
onChange={(e) => {
setSmppPass(e.target.value);
}}
/>
</ScopedAccess>
<label htmlFor="outbound_smpp">
Carrier SMPP Gateways
<span>{smppSystemId || smppPass ? "*" : ""}</span>
@@ -1179,86 +1203,103 @@ export const CarrierForm = ({
</label>
</div>
</div>
<button
title="Delete Outbound SMPP Gateway"
type="button"
className="btnty"
onClick={() => {
setSmppOutboundMessage("");
if (
hasLength(smpps) &&
smppGateways.filter((g) => g.outbound).length <=
1 &&
(smppSystemId || smppPass)
) {
setSmppOutboundMessage(
"You must provide at least one Outbound Gateway."
);
} else {
handleSmppGatewayDelete(
smppGateways.find((g2, i2) => i2 === i)
);
setSmppGateways(
smppGateways.filter((g2, i2) => i2 !== i)
);
}
}}
<ScopedAccess
user={user}
scope={
accountSid ? Scope.account : Scope.service_provider
}
>
<Icon>
<Icons.Trash2 />
</Icon>
</button>
<button
title="Delete Outbound SMPP Gateway"
type="button"
className="btnty"
onClick={() => {
setSmppOutboundMessage("");
if (
hasLength(smpps) &&
smppGateways.filter((g) => g.outbound).length <=
1 &&
(smppSystemId || smppPass)
) {
setSmppOutboundMessage(
"You must provide at least one Outbound Gateway."
);
} else {
handleSmppGatewayDelete(
smppGateways.find((g2, i2) => i2 === i)
);
setSmppGateways(
smppGateways.filter((g2, i2) => i2 !== i)
);
}
}}
>
<Icon>
<Icons.Trash2 />
</Icon>
</button>
</ScopedAccess>
</div>
) : null;
})}
<ButtonGroup left>
<button
className="btnty"
type="button"
onClick={() => addSmppGateway({ inbound: 0, outbound: 1 })}
title="Add Outbound SMPP Gateway"
>
<Icon subStyle="teal">
<Icons.Plus />
</Icon>
</button>
</ButtonGroup>
<ScopedAccess
user={user}
scope={accountSid ? Scope.account : Scope.service_provider}
>
<ButtonGroup left>
<button
className="btnty"
type="button"
onClick={() => addSmppGateway({ inbound: 0, outbound: 1 })}
title="Add Outbound SMPP Gateway"
>
<Icon subStyle="teal">
<Icons.Plus />
</Icon>
</button>
</ButtonGroup>
</ScopedAccess>
</fieldset>
<fieldset>
<label htmlFor="inbound_smpp">Inbound SMPP</label>
<label htmlFor="inbound_id">System ID</label>
<input
id="inbound_id"
name="inbound_id"
type="text"
value={smppInboundSystemId}
placeholder="SMPP system ID to authenticate with"
onChange={(e) => {
setSmppInboundSystemId(e.target.value);
}}
/>
<label htmlFor="inbound_pass">
Password
<span>{!hasEmptySmppGateways("inbound") ? "*" : ""}</span>
</label>
<MXS>
<em>
Password is required if whitelisting carrier IP address(es)
below.
</em>
</MXS>
<Passwd
id="inbound_pass"
name="inbound_pass"
value={smppInboundPass}
placeholder="SMPP password for authenticating inbound messages"
required={!hasEmptySmppGateways("inbound")}
onChange={(e) => {
setSmppInboundPass(e.target.value);
}}
/>
<ScopedAccess
user={user}
scope={accountSid ? Scope.account : Scope.service_provider}
>
<label htmlFor="inbound_id">System ID</label>
<input
id="inbound_id"
name="inbound_id"
type="text"
value={smppInboundSystemId}
placeholder="SMPP system ID to authenticate with"
onChange={(e) => {
setSmppInboundSystemId(e.target.value);
}}
/>
<label htmlFor="inbound_pass">
Password
<span>{!hasEmptySmppGateways("inbound") ? "*" : ""}</span>
</label>
<MXS>
<em>
Password is required if whitelisting carrier IP address(es)
below.
</em>
</MXS>
<Passwd
id="inbound_pass"
name="inbound_pass"
value={smppInboundPass}
placeholder="SMPP password for authenticating inbound messages"
required={!hasEmptySmppGateways("inbound")}
onChange={(e) => {
setSmppInboundPass(e.target.value);
}}
/>
</ScopedAccess>
<label htmlFor="inbound_smpp">
Carrier IP Address(es) to whitelist
</label>
@@ -1304,39 +1345,51 @@ export const CarrierForm = ({
/>
</div>
</div>
<button
className="btnty"
title="Delete Inbound SMPP Gateway"
type="button"
onClick={() => {
handleSmppGatewayDelete(
smppGateways.find((g2, i2) => i2 === i)
);
setSmppGateways(
smppGateways.filter((g2, i2) => i2 !== i)
);
}}
<ScopedAccess
user={user}
scope={
accountSid ? Scope.account : Scope.service_provider
}
>
<Icon>
<Icons.Trash2 />
</Icon>
</button>
<button
className="btnty"
title="Delete Inbound SMPP Gateway"
type="button"
onClick={() => {
handleSmppGatewayDelete(
smppGateways.find((g2, i2) => i2 === i)
);
setSmppGateways(
smppGateways.filter((g2, i2) => i2 !== i)
);
}}
>
<Icon>
<Icons.Trash2 />
</Icon>
</button>
</ScopedAccess>
</div>
) : null;
})}
<ButtonGroup left>
<button
className="btnty"
type="button"
onClick={() => addSmppGateway({ outbound: 0, inbound: 1 })}
title="Add Inbound SMPP Gateway"
>
<Icon subStyle="teal">
<Icons.Plus />
</Icon>
</button>
</ButtonGroup>
<ScopedAccess
user={user}
scope={accountSid ? Scope.account : Scope.service_provider}
>
<ButtonGroup left>
<button
className="btnty"
type="button"
onClick={() => addSmppGateway({ outbound: 0, inbound: 1 })}
title="Add Inbound SMPP Gateway"
>
<Icon subStyle="teal">
<Icons.Plus />
</Icon>
</button>
</ButtonGroup>
</ScopedAccess>
</fieldset>
</Tab>
</Tabs>
@@ -125,7 +125,7 @@ export const RecentCalls = () => {
) : hasLength(calls) ? (
calls.map((call) => <DetailsItem key={call.call_sid} call={call} />)
) : (
<M>No data</M>
<M>No data.</M>
)}
</div>
</Section>
@@ -5,12 +5,14 @@ import { withSelectState } from "src/utils";
import { ApiKeys } from "src/containers/internal/api-keys";
import ServiceProviderSettings from "./service-provider-settings";
import AdminSettings from "./admin-settings";
import { ScopedAccess } from "src/components/scoped-access";
import type { ServiceProvider } from "src/api/types";
import { Section } from "src/components";
import { USER_ADMIN } from "src/api/constants";
import { MSG_REQUIRED_FIELDS } from "src/constants";
import { ScopedAccess } from "src/components/scoped-access";
import { Scope } from "src/store/types";
import { useSelectState } from "src/store";
import { Scope } from "src/store/types";
type SettingsProps = {
currentServiceProvider: ServiceProvider;
@@ -38,9 +40,11 @@ export const Settings = ({ currentServiceProvider }: SettingsProps) => {
</Tab>
</Tabs>
</ScopedAccess>
<ScopedAccess scope={Scope.service_provider} user={user}>
<ServiceProviderSettings />
</ScopedAccess>
{user?.scope !== USER_ADMIN && (
<ScopedAccess scope={Scope.service_provider} user={user}>
<ServiceProviderSettings />
</ScopedAccess>
)}
</form>
</Section>
@@ -575,7 +575,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
}}
checked={useCustomTts}
/>
<div>Use for custom text-to-speech</div>
<div>Use for custom voice</div>
</label>
<label htmlFor="use_custom_tts">
Custom voice endpoint{useCustomTts && <span>*</span>}
@@ -611,7 +611,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
}}
checked={useCustomStt}
/>
<div>Use for custom speech-to-text</div>
<div>Use for custom speech model</div>
</label>
<label htmlFor="use_custom_stt">
Custom speech endpoint id{useCustomStt && <span>*</span>}
+4 -2
View File
@@ -52,7 +52,9 @@ export const UserForm = ({ user }: UserFormProps) => {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [initialPassword, setInitialPassword] = useState("");
const [scope, setScope] = useState<UserScopes>();
const [scope, setScope] = useState<UserScopes | null>(
currentUser?.scope || null
);
const [isActive, setIsActive] = useState(true);
const [forceChange, setForceChange] = useState(true);
const [modal, setModal] = useState(false);
@@ -184,7 +186,7 @@ export const UserForm = ({ user }: UserFormProps) => {
<Selector
id="scope"
name="scope"
value={scope}
value={scope || currentUser?.scope}
options={
currentUser?.scope === USER_SP
? USER_SCOPE_SELECTION.filter(
+25 -3
View File
@@ -18,7 +18,12 @@ import {
AccountFilter,
SelectFilter,
} from "src/components";
import { hasLength, hasValue, useFilteredResults } from "src/utils";
import {
hasLength,
hasValue,
sortAlphabetically,
useFilteredResults,
} from "src/utils";
import type { Account, User } from "src/api/types";
import { useSelectState } from "src/store";
@@ -33,6 +38,15 @@ export const Users = () => {
const [accounts] = useServiceProviderData<Account[]>("Accounts");
const usersFiltered = useMemo(() => {
//find and add account/sp names to user to improve fuzzy search
users?.forEach((user) => {
user.account_name =
accounts?.find((acct) => acct.account_sid === user.account_sid)?.name ||
null;
user.service_provider_name =
user.scope === USER_ADMIN ? null : currentServiceProvider?.name || null;
});
const serviceProviderUsers = users?.filter((e) => {
return (
e.scope === USER_ADMIN ||
@@ -60,7 +74,9 @@ export const Users = () => {
return [];
}, [accountSid, scopeFilter, users, accounts, currentServiceProvider]);
const filteredUsers = useFilteredResults<User>(filter, usersFiltered);
const filteredUsers = useFilteredResults<User>(filter, usersFiltered)?.sort(
(a, b) => sortAlphabetically(a, b)
);
return (
<>
@@ -99,11 +115,12 @@ export const Users = () => {
</section>
<Section slim>
<div className="grid grid--col4">
<div className="grid grid--col5">
<div className="grid__row grid__th">
<div>User Name</div>
<div>Email</div>
<div>Scope</div>
<div>Access</div>
<div>&nbsp;</div>
</div>
{!hasValue(users) ? (
@@ -115,6 +132,11 @@ export const Users = () => {
<div>{user.name}</div>
<div>{user.email}</div>
<div>{user.scope}</div>
<div>
{user.scope === USER_ADMIN
? null
: user.account_name || user.service_provider_name}
</div>
<div className="item__actions">
<Link
to={`${ROUTE_INTERNAL_USERS}/${user.user_sid}/edit`}
+3 -4
View File
@@ -99,17 +99,16 @@
}
}
&--col4 {
&--col5 {
.grid__row {
grid-template-columns: [col] 30% [col] 30% [col] 30% [col] 10%;
grid-template-columns: [col] 30% [col] 20% [col] 20% [col] 28% [col] 2%;
grid-template-rows: [row] auto [row] auto [row] [row] auto;
display: grid;
justify-content: space-between;
> div:last-child {
text-align: right;
text-align: left;
padding-right: 0;
font-size: 0;
}
}
}
+23
View File
@@ -0,0 +1,23 @@
@use "./vars";
@use "./mixins";
@use "jambonz-ui/src/styles/vars" as ui-vars;
@use "jambonz-ui/src/styles/mixins" as ui-mixins;
.user {
padding: ui-vars.$px03;
display: flex;
align-items: center;
justify-content: space-between;
font-size: ui-vars.$mxs-size;
line-height: 0.4rem;
&--icon {
color: ui-vars.$jambonz;
width: ui-vars.$h3-size;
height: ui-vars.$h3-size;
padding-right: ui-vars.$px00;
}
&--txt {
padding-top: ui-vars.$px01;
}
}
+1
View File
@@ -5,6 +5,7 @@
@use "./inpbtn";
@use "./filters";
@use "./grid";
@use "./user";
@use "jambonz-ui/src/styles/index";
@use "./vars";
+14
View File
@@ -148,6 +148,20 @@ export const getUserScope = (user: User): UserScopes => {
}
};
export const sortAlphabetically = (a: User, b: User) => {
const nameA = a.name.toUpperCase(); // ignore upper and lowercase
const nameB = b.name.toUpperCase(); // ignore upper and lowercase
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
// names must be equal
return 0;
};
export {
withSuspense,
useMobileMedia,