@reown/appkit-react-native 0.0.0-feat-multichain-phantom-20250606201606 → 0.0.0-feat-multi-siwe-20250619154334
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/AppKit.js +63 -7
- package/lib/commonjs/AppKit.js.map +1 -1
- package/lib/commonjs/connectors/WalletConnectConnector.js +79 -6
- package/lib/commonjs/connectors/WalletConnectConnector.js.map +1 -1
- package/lib/commonjs/modal/w3m-modal/index.js +7 -47
- package/lib/commonjs/modal/w3m-modal/index.js.map +1 -1
- package/lib/commonjs/modal/w3m-router/index.js +2 -2
- package/lib/commonjs/modal/w3m-router/index.js.map +1 -1
- package/lib/commonjs/views/w3m-connecting-siwe-view/index.js +138 -0
- package/lib/commonjs/views/w3m-connecting-siwe-view/index.js.map +1 -0
- package/lib/commonjs/views/w3m-connecting-siwe-view/styles.js +35 -0
- package/lib/commonjs/views/w3m-connecting-siwe-view/styles.js.map +1 -0
- package/lib/module/AppKit.js +63 -8
- package/lib/module/AppKit.js.map +1 -1
- package/lib/module/connectors/WalletConnectConnector.js +79 -6
- package/lib/module/connectors/WalletConnectConnector.js.map +1 -1
- package/lib/module/modal/w3m-modal/index.js +8 -48
- package/lib/module/modal/w3m-modal/index.js.map +1 -1
- package/lib/module/modal/w3m-router/index.js +1 -1
- package/lib/module/modal/w3m-router/index.js.map +1 -1
- package/lib/module/views/w3m-connecting-siwe-view/index.js +131 -0
- package/lib/module/views/w3m-connecting-siwe-view/index.js.map +1 -0
- package/lib/module/views/w3m-connecting-siwe-view/styles.js +29 -0
- package/lib/module/views/w3m-connecting-siwe-view/styles.js.map +1 -0
- package/lib/typescript/AppKit.d.ts +6 -3
- package/lib/typescript/AppKit.d.ts.map +1 -1
- package/lib/typescript/AppKitContext.d.ts +1 -1
- package/lib/typescript/connectors/WalletConnectConnector.d.ts.map +1 -1
- package/lib/typescript/modal/w3m-modal/index.d.ts.map +1 -1
- package/lib/typescript/views/w3m-connecting-siwe-view/index.d.ts +2 -0
- package/lib/typescript/views/w3m-connecting-siwe-view/index.d.ts.map +1 -0
- package/lib/typescript/views/w3m-connecting-siwe-view/styles.d.ts +28 -0
- package/lib/typescript/views/w3m-connecting-siwe-view/styles.d.ts.map +1 -0
- package/package.json +5 -5
- package/src/AppKit.ts +63 -9
- package/src/connectors/WalletConnectConnector.ts +80 -6
- package/src/modal/w3m-modal/index.tsx +10 -60
- package/src/modal/w3m-router/index.tsx +1 -1
- package/src/views/w3m-connecting-siwe-view/index.tsx +142 -0
- package/src/views/w3m-connecting-siwe-view/styles.ts +29 -0
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type ConnectorInitOptions,
|
|
13
13
|
type Metadata
|
|
14
14
|
} from '@reown/appkit-common-react-native';
|
|
15
|
+
import { getDidAddress, getDidChainId, SIWEController } from '@reown/appkit-siwe-react-native';
|
|
15
16
|
|
|
16
17
|
interface WalletConnectConnectorConfig {
|
|
17
18
|
projectId: string;
|
|
@@ -85,6 +86,7 @@ export class WalletConnectConnector extends WalletConnector {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
override async connect(opts: ConnectOptions) {
|
|
89
|
+
const { siweConfig, namespaces, defaultChain, universalLink } = opts;
|
|
88
90
|
function onUri(uri: string) {
|
|
89
91
|
ConnectionController.setWcUri(uri);
|
|
90
92
|
}
|
|
@@ -94,13 +96,84 @@ export class WalletConnectConnector extends WalletConnector {
|
|
|
94
96
|
// @ts-ignore
|
|
95
97
|
provider.on('display_uri', onUri);
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
let session;
|
|
100
|
+
|
|
101
|
+
// SIWE
|
|
102
|
+
const params = await siweConfig?.getMessageParams?.();
|
|
103
|
+
if (siweConfig?.options?.enabled && params && Object.keys(params).length > 0) {
|
|
104
|
+
// @ts-ignore
|
|
105
|
+
const result = await provider.authenticate(
|
|
106
|
+
{
|
|
107
|
+
...params,
|
|
108
|
+
nonce: await siweConfig.getNonce(),
|
|
109
|
+
methods: namespaces?.['eip155']?.methods,
|
|
110
|
+
chains: params.chains.map(chain => `eip155:${chain}`)
|
|
111
|
+
},
|
|
112
|
+
universalLink
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
console.log('result SIWE', result);
|
|
116
|
+
|
|
117
|
+
// Auths is an array of signed CACAO objects https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-74.md
|
|
118
|
+
const signedCacao = result?.auths?.[0];
|
|
119
|
+
if (signedCacao) {
|
|
120
|
+
const { p, s } = signedCacao;
|
|
121
|
+
const chainId = getDidChainId(p.iss);
|
|
122
|
+
const address = getDidAddress(p.iss);
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// Kicks off verifyMessage and populates external states
|
|
126
|
+
const message = provider?.client?.formatAuthMessage({
|
|
127
|
+
request: p,
|
|
128
|
+
iss: p.iss
|
|
129
|
+
})!;
|
|
130
|
+
|
|
131
|
+
await SIWEController.verifyMessage({
|
|
132
|
+
message,
|
|
133
|
+
signature: s.s,
|
|
134
|
+
cacao: signedCacao
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (address && chainId) {
|
|
138
|
+
const session = {
|
|
139
|
+
address,
|
|
140
|
+
chainId: parseInt(chainId, 10)
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
SIWEController.setSession(session);
|
|
144
|
+
SIWEController.onSignIn?.(session);
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// eslint-disable-next-line no-console
|
|
148
|
+
console.error('Error verifying message', error);
|
|
149
|
+
// eslint-disable-next-line no-console
|
|
150
|
+
await provider.disconnect().catch(console.error);
|
|
151
|
+
// eslint-disable-next-line no-console
|
|
152
|
+
await SIWEController.signOut().catch(console.error);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
session = result?.session;
|
|
157
|
+
} else {
|
|
158
|
+
session = await (this.provider as IUniversalProvider).connect({
|
|
159
|
+
namespaces: {},
|
|
160
|
+
optionalNamespaces: namespaces
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const metadata = session?.peer?.metadata;
|
|
165
|
+
if (metadata) {
|
|
166
|
+
this.wallet = {
|
|
167
|
+
...metadata,
|
|
168
|
+
name: metadata?.name,
|
|
169
|
+
icon: metadata?.icons?.[0]
|
|
170
|
+
};
|
|
171
|
+
} else {
|
|
172
|
+
this.wallet = undefined;
|
|
173
|
+
}
|
|
101
174
|
|
|
102
|
-
if (
|
|
103
|
-
(this.provider as IUniversalProvider).setDefaultChain(
|
|
175
|
+
if (defaultChain) {
|
|
176
|
+
(this.provider as IUniversalProvider).setDefaultChain(defaultChain);
|
|
104
177
|
}
|
|
105
178
|
|
|
106
179
|
this.namespaces = session?.namespaces as Namespaces;
|
|
@@ -133,6 +206,7 @@ export class WalletConnectConnector extends WalletConnector {
|
|
|
133
206
|
}
|
|
134
207
|
|
|
135
208
|
override getWalletInfo(): WalletInfo | undefined {
|
|
209
|
+
console.log('getWalletInfo', this.wallet);
|
|
136
210
|
return this.wallet;
|
|
137
211
|
}
|
|
138
212
|
|
|
@@ -4,21 +4,17 @@ import { useWindowDimensions, StatusBar } from 'react-native';
|
|
|
4
4
|
import Modal from 'react-native-modal';
|
|
5
5
|
import { Card, ThemeProvider } from '@reown/appkit-ui-react-native';
|
|
6
6
|
import {
|
|
7
|
-
AccountController,
|
|
8
7
|
ApiController,
|
|
9
|
-
ConnectionController,
|
|
10
8
|
ConnectorController,
|
|
11
|
-
CoreHelperUtil,
|
|
12
9
|
EventsController,
|
|
13
10
|
ModalController,
|
|
14
11
|
OptionsController,
|
|
15
12
|
RouterController,
|
|
16
|
-
TransactionsController,
|
|
17
13
|
type AppKitFrameProvider,
|
|
18
14
|
WebviewController,
|
|
19
|
-
ThemeController
|
|
15
|
+
ThemeController,
|
|
16
|
+
ConnectionsController
|
|
20
17
|
} from '@reown/appkit-core-react-native';
|
|
21
|
-
import type { CaipAddress } from '@reown/appkit-common-react-native';
|
|
22
18
|
import { SIWEController } from '@reown/appkit-siwe-react-native';
|
|
23
19
|
|
|
24
20
|
import { AppKitRouter } from '../w3m-router';
|
|
@@ -26,11 +22,12 @@ import { Header } from '../../partials/w3m-header';
|
|
|
26
22
|
import { Snackbar } from '../../partials/w3m-snackbar';
|
|
27
23
|
import { useCustomDimensions } from '../../hooks/useCustomDimensions';
|
|
28
24
|
import styles from './styles';
|
|
25
|
+
import { useAppKit } from '../../AppKitContext';
|
|
29
26
|
|
|
30
27
|
export function AppKit() {
|
|
31
|
-
const {
|
|
28
|
+
const { disconnect } = useAppKit();
|
|
29
|
+
const { open } = useSnapshot(ModalController.state);
|
|
32
30
|
const { connectors, connectedConnector } = useSnapshot(ConnectorController.state);
|
|
33
|
-
const { caipAddress, isConnected } = useSnapshot(AccountController.state);
|
|
34
31
|
const { frameViewVisible, webviewVisible } = useSnapshot(WebviewController.state);
|
|
35
32
|
const { themeMode, themeVariables } = useSnapshot(ThemeController.state);
|
|
36
33
|
const { projectId } = useSnapshot(OptionsController.state);
|
|
@@ -58,8 +55,11 @@ export function AppKit() {
|
|
|
58
55
|
|
|
59
56
|
const handleClose = async () => {
|
|
60
57
|
if (OptionsController.state.isSiweEnabled) {
|
|
61
|
-
if (
|
|
62
|
-
|
|
58
|
+
if (
|
|
59
|
+
SIWEController.state.status !== 'success' &&
|
|
60
|
+
!!ConnectionsController.state.activeAddress
|
|
61
|
+
) {
|
|
62
|
+
await disconnect();
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -71,62 +71,12 @@ export function AppKit() {
|
|
|
71
71
|
EventsController.sendEvent({ type: 'track', event: 'BUY_CANCEL' });
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
|
-
|
|
75
|
-
const onNewAddress = useCallback(
|
|
76
|
-
async (address?: CaipAddress) => {
|
|
77
|
-
if (!isConnected || loading) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const newAddress = CoreHelperUtil.getPlainAddress(address);
|
|
82
|
-
TransactionsController.resetTransactions();
|
|
83
|
-
|
|
84
|
-
if (OptionsController.state.isSiweEnabled) {
|
|
85
|
-
const newNetworkId = CoreHelperUtil.getNetworkId(address);
|
|
86
|
-
|
|
87
|
-
const { signOutOnAccountChange, signOutOnNetworkChange } =
|
|
88
|
-
SIWEController.state._client?.options ?? {};
|
|
89
|
-
const session = await SIWEController.getSession();
|
|
90
|
-
|
|
91
|
-
if (session && newAddress && signOutOnAccountChange) {
|
|
92
|
-
// If the address has changed and signOnAccountChange is enabled, sign out
|
|
93
|
-
await SIWEController.signOut();
|
|
94
|
-
onSiweNavigation();
|
|
95
|
-
} else if (
|
|
96
|
-
newNetworkId &&
|
|
97
|
-
session?.chainId.toString() !== newNetworkId &&
|
|
98
|
-
signOutOnNetworkChange
|
|
99
|
-
) {
|
|
100
|
-
// If the network has changed and signOnNetworkChange is enabled, sign out
|
|
101
|
-
await SIWEController.signOut();
|
|
102
|
-
onSiweNavigation();
|
|
103
|
-
} else if (!session) {
|
|
104
|
-
// If it's connected but there's no session, show sign view
|
|
105
|
-
onSiweNavigation();
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
[isConnected, loading]
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
const onSiweNavigation = () => {
|
|
113
|
-
if (ModalController.state.open) {
|
|
114
|
-
RouterController.push('ConnectingSiwe');
|
|
115
|
-
} else {
|
|
116
|
-
ModalController.open({ view: 'ConnectingSiwe' });
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
|
|
120
74
|
useEffect(() => {
|
|
121
75
|
if (projectId) {
|
|
122
76
|
prefetch();
|
|
123
77
|
}
|
|
124
78
|
}, [projectId, prefetch]);
|
|
125
79
|
|
|
126
|
-
useEffect(() => {
|
|
127
|
-
onNewAddress(caipAddress);
|
|
128
|
-
}, [caipAddress, onNewAddress]);
|
|
129
|
-
|
|
130
80
|
return (
|
|
131
81
|
<>
|
|
132
82
|
<ThemeProvider themeMode={themeMode} themeVariables={themeVariables}>
|
|
@@ -12,7 +12,7 @@ import { ConnectingExternalView } from '../../views/w3m-connecting-external-view
|
|
|
12
12
|
import { ConnectingFarcasterView } from '../../views/w3m-connecting-farcaster-view';
|
|
13
13
|
import { ConnectingSocialView } from '../../views/w3m-connecting-social-view';
|
|
14
14
|
import { CreateView } from '../../views/w3m-create-view';
|
|
15
|
-
import { ConnectingSiweView } from '
|
|
15
|
+
import { ConnectingSiweView } from '../../views/w3m-connecting-siwe-view';
|
|
16
16
|
import { EmailVerifyOtpView } from '../../views/w3m-email-verify-otp-view';
|
|
17
17
|
import { EmailVerifyDeviceView } from '../../views/w3m-email-verify-device-view';
|
|
18
18
|
import { GetWalletView } from '../../views/w3m-get-wallet-view';
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { useSnapshot } from 'valtio';
|
|
2
|
+
import {
|
|
3
|
+
Avatar,
|
|
4
|
+
Button,
|
|
5
|
+
DoubleImageLoader,
|
|
6
|
+
FlexView,
|
|
7
|
+
IconLink,
|
|
8
|
+
Text
|
|
9
|
+
} from '@reown/appkit-ui-react-native';
|
|
10
|
+
import {
|
|
11
|
+
AccountController,
|
|
12
|
+
AssetUtil,
|
|
13
|
+
ConnectionController,
|
|
14
|
+
ConnectionsController,
|
|
15
|
+
EventsController,
|
|
16
|
+
ModalController,
|
|
17
|
+
OptionsController,
|
|
18
|
+
RouterController,
|
|
19
|
+
SnackController
|
|
20
|
+
} from '@reown/appkit-core-react-native';
|
|
21
|
+
|
|
22
|
+
import { useState } from 'react';
|
|
23
|
+
import { SIWEController } from '@reown/appkit-siwe-react-native';
|
|
24
|
+
import styles from './styles';
|
|
25
|
+
import { useAppKit } from '../../AppKitContext';
|
|
26
|
+
|
|
27
|
+
export function ConnectingSiweView() {
|
|
28
|
+
const { disconnect } = useAppKit();
|
|
29
|
+
const { metadata } = useSnapshot(OptionsController.state);
|
|
30
|
+
const { connectedWalletImageUrl, pressedWallet } = useSnapshot(ConnectionController.state);
|
|
31
|
+
const { activeAddress } = useSnapshot(ConnectionsController.state);
|
|
32
|
+
const [isSigning, setIsSigning] = useState(false);
|
|
33
|
+
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
|
34
|
+
|
|
35
|
+
const dappName = metadata?.name || 'Dapp';
|
|
36
|
+
const dappIcon = metadata?.icons[0] || '';
|
|
37
|
+
const walletIcon = AssetUtil.getWalletImage(pressedWallet) || connectedWalletImageUrl;
|
|
38
|
+
|
|
39
|
+
const onSign = async () => {
|
|
40
|
+
setIsSigning(true);
|
|
41
|
+
EventsController.sendEvent({
|
|
42
|
+
event: 'CLICK_SIGN_SIWE_MESSAGE',
|
|
43
|
+
type: 'track',
|
|
44
|
+
properties: {
|
|
45
|
+
network: ConnectionsController.state.activeNetwork?.caipNetworkId || '',
|
|
46
|
+
isSmartAccount: AccountController.state.preferredAccountType === 'smartAccount'
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
try {
|
|
50
|
+
const session = await SIWEController.signIn();
|
|
51
|
+
|
|
52
|
+
EventsController.sendEvent({
|
|
53
|
+
event: 'SIWE_AUTH_SUCCESS',
|
|
54
|
+
type: 'track',
|
|
55
|
+
properties: {
|
|
56
|
+
network: ConnectionsController.state.activeNetwork?.caipNetworkId || '',
|
|
57
|
+
isSmartAccount: AccountController.state.preferredAccountType === 'smartAccount'
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return session;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
SnackController.showError('Signature declined');
|
|
64
|
+
|
|
65
|
+
SIWEController.setStatus('error');
|
|
66
|
+
|
|
67
|
+
return EventsController.sendEvent({
|
|
68
|
+
event: 'SIWE_AUTH_ERROR',
|
|
69
|
+
type: 'track',
|
|
70
|
+
properties: {
|
|
71
|
+
network: ConnectionsController.state.activeNetwork?.caipNetworkId || '',
|
|
72
|
+
isSmartAccount: AccountController.state.preferredAccountType === 'smartAccount'
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
} finally {
|
|
76
|
+
setIsSigning(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const onCancel = async () => {
|
|
81
|
+
if (ConnectionsController.state.activeAddress) {
|
|
82
|
+
setIsDisconnecting(true);
|
|
83
|
+
await disconnect();
|
|
84
|
+
ModalController.close();
|
|
85
|
+
setIsDisconnecting(false);
|
|
86
|
+
} else {
|
|
87
|
+
RouterController.push('Connect');
|
|
88
|
+
}
|
|
89
|
+
EventsController.sendEvent({
|
|
90
|
+
event: 'CLICK_CANCEL_SIWE',
|
|
91
|
+
type: 'track',
|
|
92
|
+
properties: {
|
|
93
|
+
network: ConnectionsController.state.activeNetwork?.caipNetworkId || '',
|
|
94
|
+
isSmartAccount: AccountController.state.preferredAccountType === 'smartAccount'
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<FlexView padding={['2xl', 's', '3xl', 's']}>
|
|
101
|
+
<IconLink
|
|
102
|
+
icon="close"
|
|
103
|
+
size="md"
|
|
104
|
+
onPress={onCancel}
|
|
105
|
+
testID="header-close"
|
|
106
|
+
style={styles.closeButton}
|
|
107
|
+
/>
|
|
108
|
+
<Text variant="paragraph-600" numberOfLines={1} center>
|
|
109
|
+
Sign in
|
|
110
|
+
</Text>
|
|
111
|
+
<DoubleImageLoader
|
|
112
|
+
style={styles.logoContainer}
|
|
113
|
+
leftImage={dappIcon}
|
|
114
|
+
rightImage={walletIcon}
|
|
115
|
+
renderRightPlaceholder={() => (
|
|
116
|
+
<Avatar imageSrc={undefined} address={activeAddress} size={60} borderWidth={0} />
|
|
117
|
+
)}
|
|
118
|
+
rightItemStyle={!walletIcon && styles.walletAvatar}
|
|
119
|
+
/>
|
|
120
|
+
<Text center variant="medium-600" color="fg-100" style={styles.title}>
|
|
121
|
+
{dappName} needs to connect to your wallet
|
|
122
|
+
</Text>
|
|
123
|
+
<Text center variant="small-400" color="fg-200" style={styles.subtitle}>
|
|
124
|
+
Sign this message to prove you own this wallet and proceed. Cancelling will disconnect you
|
|
125
|
+
</Text>
|
|
126
|
+
<FlexView flexDirection="row" justifyContent="space-between" margin={['s', '0', '0', '0']}>
|
|
127
|
+
<Button variant="shade" onPress={onCancel} style={styles.button} loading={isDisconnecting}>
|
|
128
|
+
Cancel
|
|
129
|
+
</Button>
|
|
130
|
+
<Button
|
|
131
|
+
variant="fill"
|
|
132
|
+
loading={isSigning}
|
|
133
|
+
disabled={isDisconnecting}
|
|
134
|
+
onPress={onSign}
|
|
135
|
+
style={styles.button}
|
|
136
|
+
>
|
|
137
|
+
Sign
|
|
138
|
+
</Button>
|
|
139
|
+
</FlexView>
|
|
140
|
+
</FlexView>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BorderRadius, Spacing } from '@reown/appkit-ui-react-native';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export default StyleSheet.create({
|
|
5
|
+
logoContainer: {
|
|
6
|
+
marginTop: Spacing.xl,
|
|
7
|
+
marginBottom: Spacing.m
|
|
8
|
+
},
|
|
9
|
+
button: {
|
|
10
|
+
width: '48%'
|
|
11
|
+
},
|
|
12
|
+
title: {
|
|
13
|
+
marginHorizontal: '15%'
|
|
14
|
+
},
|
|
15
|
+
subtitle: {
|
|
16
|
+
marginHorizontal: '10%',
|
|
17
|
+
marginVertical: Spacing.l
|
|
18
|
+
},
|
|
19
|
+
closeButton: {
|
|
20
|
+
alignSelf: 'flex-end',
|
|
21
|
+
right: Spacing.xl,
|
|
22
|
+
top: Spacing.l,
|
|
23
|
+
position: 'absolute',
|
|
24
|
+
zIndex: 2
|
|
25
|
+
},
|
|
26
|
+
walletAvatar: {
|
|
27
|
+
borderRadius: BorderRadius.full
|
|
28
|
+
}
|
|
29
|
+
});
|