Skip to content

Commit c28ad05

Browse files
committed
feat(mask): fw-6741 centralize extension permissions & add request UI
move permission constants to shared defs, use them in oauth helper and firefly wallet flow, and add a runtime permission dialog to request origins.
1 parent 3a0154d commit c28ad05

File tree

6 files changed

+158
-34
lines changed

6 files changed

+158
-34
lines changed

packages/mask/background/services/helper/oauth-x.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { timeout } from '@masknet/kit'
22
import { requestExtensionPermissionFromContentScript } from './request-permission.js'
3+
import { XOAuthRequestOrigins } from '../../../shared/definitions/extension.js'
34

45
/** Modified from https://github.com/ddo/oauth-1.0a/blob/master/oauth-1.0a.js */
56
class OAuth {
@@ -184,15 +185,7 @@ const client = new OAuth(process.env.FIREFLY_X_CLIENT_ID, process.env.FIREFLY_X_
184185
let pendingOAuth: PromiseWithResolvers<{ oauth_verifier: string; oauth_token: string }> | undefined
185186
export async function requestXOAuthToken() {
186187
await requestExtensionPermissionFromContentScript({
187-
origins: [
188-
// In order to send API request without CORS limit
189-
'https://api.twitter.com/*',
190-
// In order to run content script on it
191-
'https://firefly.social/api/mask/delegate-x-token',
192-
'https://firefly.social/api/auth/callback/twitter',
193-
'https://canary.firefly.social/api/mask/delegate-x-token',
194-
'https://canary.firefly.social/api/auth/callback/twitter',
195-
],
188+
origins: XOAuthRequestOrigins,
196189
})
197190
await Promise.all([
198191
fetch('https://firefly.social/api/mask/delegate-x-token'),

packages/mask/dashboard/pages/CreateMaskWallet/FireflyWallet/index.tsx

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import Services from '#services'
22
import { Trans } from '@lingui/react/macro'
33
import { Icons } from '@masknet/icons'
4+
import { PopupRoutes } from '@masknet/shared-base'
45
import { makeStyles } from '@masknet/theme'
5-
import { Typography } from '@mui/material'
6-
import { memo } from 'react'
6+
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material'
7+
import { memo, useState } from 'react'
8+
import { useAsyncFn, useAsyncRetry } from 'react-use'
79
import { PrimaryButton } from '../../../components/PrimaryButton/index.js'
810
import { SetupFrameController } from '../../../components/SetupFrame/index.js'
9-
import { useAsyncFn } from 'react-use'
10-
import { PopupRoutes } from '@masknet/shared-base'
11+
import { XOAuthRequestOrigins } from '../../../../shared/definitions/extension.js'
1112

1213
const useStyles = makeStyles()((theme) => ({
1314
container: {
@@ -63,20 +64,79 @@ const useStyles = makeStyles()((theme) => ({
6364
listStyle: 'disc',
6465
},
6566
},
67+
dialog: {
68+
width: 600,
69+
boxSizing: 'border-box',
70+
padding: 24,
71+
borderRadius: 12,
72+
display: 'flex',
73+
flexDirection: 'column',
74+
gap: 24,
75+
},
76+
dialogTitle: {
77+
color: theme.palette.maskColor.main,
78+
fontSize: 18,
79+
fontWeight: 700,
80+
lineHeight: '22px',
81+
padding: 0,
82+
},
83+
dialogContent: {
84+
display: 'flex',
85+
flexDirection: 'column',
86+
gap: 12,
87+
padding: 0,
88+
},
89+
permissions: {
90+
backgroundColor: theme.palette.maskColor.bg,
91+
padding: 12,
92+
borderRadius: 8,
93+
height: 212,
94+
boxSizing: 'border-box',
95+
minHeight: 0,
96+
flexGrow: 1,
97+
overflow: 'auto',
98+
},
99+
dialogActions: {
100+
display: 'flex',
101+
justifyContent: 'center',
102+
gap: 12,
103+
padding: 0,
104+
},
105+
actionButton: {
106+
minWidth: 110,
107+
'&&': {
108+
marginLeft: 0,
109+
},
110+
},
66111
}))
67112

68113
export const Component = memo(function CreateWalletForm() {
69114
const { classes, cx } = useStyles()
115+
const [open, setOpen] = useState(false)
116+
117+
const { retry, value: hasPermission } = useAsyncRetry(() => {
118+
const hasPermission = browser.permissions.contains({
119+
origins: XOAuthRequestOrigins,
120+
})
121+
setOpen(!hasPermission)
122+
return hasPermission
123+
}, [])
124+
70125
const [{ loading }, request] = useAsyncFn(async () => {
126+
if (!hasPermission) {
127+
setOpen(true)
128+
return
129+
}
71130
try {
72131
const data = await Services.Helper.loginFireflyViaTwitter()
73-
console.log('login data', data)
74132
if (!data) return
75-
await Services.Helper.openPopupWindow(PopupRoutes.CreateWallet, undefined)
133+
await Services.Helper.openPopupWindow(PopupRoutes.CreateWallet, {
134+
creatingFireflyWallet: true,
135+
})
76136
} catch (err) {
77-
console.log('login error', err)
137+
console.error('Failed to login firefly', err)
78138
}
79-
}, [])
139+
}, [hasPermission])
80140

81141
return (
82142
<div className={classes.container}>
@@ -121,6 +181,41 @@ export const Component = memo(function CreateWalletForm() {
121181
<Trans>Continue</Trans>
122182
</PrimaryButton>
123183
</SetupFrameController>
184+
<Dialog
185+
open={open}
186+
PaperProps={{
187+
elevation: 0,
188+
className: classes.dialog,
189+
}}>
190+
<DialogTitle className={classes.dialogTitle}>
191+
<Trans>Mask needs the following permissions</Trans>
192+
</DialogTitle>
193+
<DialogContent className={classes.dialogContent}>
194+
<Typography>
195+
<Trans>Sites</Trans>
196+
</Typography>
197+
<div className={classes.permissions} data-hide-scrollbar>
198+
{XOAuthRequestOrigins.map((origin) => (
199+
<Typography key={origin} lineHeight="18px">
200+
{origin}
201+
</Typography>
202+
))}
203+
</div>
204+
</DialogContent>
205+
<DialogActions className={classes.dialogActions}>
206+
<Button onClick={() => setOpen(false)} variant="outlined" className={classes.actionButton}>
207+
<Trans>Cancel</Trans>
208+
</Button>
209+
<Button
210+
onClick={() => {
211+
browser.permissions.request({ origins: XOAuthRequestOrigins }).finally(retry)
212+
}}
213+
variant="contained"
214+
className={classes.actionButton}>
215+
<Trans>Approve</Trans>
216+
</Button>
217+
</DialogActions>
218+
</Dialog>
124219
</div>
125220
)
126221
})

packages/mask/popups/pages/RequestPermission/index.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,10 @@ import { Box } from '@mui/material'
22
import { useEffect } from 'react'
33
import { useLocation } from 'react-router-dom'
44
import { useAsyncRetry } from 'react-use'
5-
import { RequestPermission } from './RequestPermission.js'
65
import type { Manifest } from 'webextension-polyfill'
6+
import { CanRequestDynamically } from '../../../shared/definitions/extension.js'
7+
import { RequestPermission } from './RequestPermission.js'
78

8-
const CanRequestDynamically: readonly Manifest.OptionalPermission[] = [
9-
'clipboardRead',
10-
'clipboardWrite',
11-
'notifications',
12-
'webRequestBlocking',
13-
]
149
function canRequestDynamically(x: string): x is Manifest.OptionalPermission {
1510
return (CanRequestDynamically as string[]).includes(x)
1611
}

packages/mask/popups/pages/Wallet/components/ImportCreateWallet/index.tsx

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import Services from '#services'
22
import { Trans } from '@lingui/react/macro'
33
import { Icons } from '@masknet/icons'
4+
import { useWallets } from '@privy-io/react-auth'
45
import { timeout } from '@masknet/kit'
56
import { LoadingStatus } from '@masknet/shared'
6-
import { DashboardRoutes } from '@masknet/shared-base'
7+
import { DashboardRoutes, type NetworkPluginID, PopupRoutes } from '@masknet/shared-base'
78
import { ActionButton, makeStyles } from '@masknet/theme'
89
import { alpha, Box, Typography, type BoxProps } from '@mui/material'
9-
import { memo, useEffect } from 'react'
10-
import { useSearchParams } from 'react-router-dom'
11-
import { useAsyncFn } from 'react-use'
10+
import { memo, useCallback } from 'react'
11+
import { useNavigate, useSearchParams } from 'react-router-dom'
12+
import { useAsync, useAsyncFn } from 'react-use'
1213
import urlcat from 'urlcat'
14+
import { useChainContext } from '@masknet/web3-hooks-base'
15+
import { EVMWeb3 } from '@masknet/web3-providers'
16+
import { ProviderType } from '@masknet/web3-shared-evm'
1317

1418
const useStyles = makeStyles()((theme) => {
1519
return {
@@ -76,7 +80,9 @@ async function loginFirefly() {
7680
}
7781
export const ImportCreateWallet = memo<Props>(function ImportCreateWallet({ onChoose, ...props }) {
7882
const { classes, cx, theme } = useStyles()
79-
const [params, setParams] = useSearchParams()
83+
const [params] = useSearchParams()
84+
const { chainId } = useChainContext<NetworkPluginID.PLUGIN_EVM>()
85+
const navigate = useNavigate()
8086
const [, handleChoose] = useAsyncFn(
8187
async (route: DashboardRoutes) => {
8288
const hasPassword = await Services.Wallet.hasPassword()
@@ -97,12 +103,28 @@ export const ImportCreateWallet = memo<Props>(function ImportCreateWallet({ onCh
97103
return timeout(loginFirefly(), 3 * 60 * 1000, timeoutMessage)
98104
}, [])
99105

100-
const isCreatingFireflyWallet = !!params.get('isCreating')
101-
useEffect(() => {
106+
const isCreatingFireflyWallet = !!params.get('creatingFireflyWallet')
107+
const { wallets } = useWallets()
108+
const selectPrivyWallet = useCallback(async () => {
109+
if (!wallets.length) return
110+
if (wallets.length > 1) {
111+
navigate(PopupRoutes.SelectWallet)
112+
return
113+
}
114+
await EVMWeb3.connect({
115+
account: wallets[0].address,
116+
chainId,
117+
providerType: ProviderType.MaskWallet,
118+
})
119+
navigate(PopupRoutes.Wallet)
120+
}, [wallets, chainId])
121+
122+
useAsync(async () => {
102123
if (!isCreatingFireflyWallet) return
103-
createPrivyWallet()
104-
setParams({}, { replace: true })
105-
}, [isCreatingFireflyWallet])
124+
await createPrivyWallet()
125+
await selectPrivyWallet()
126+
}, [isCreatingFireflyWallet, selectPrivyWallet])
127+
106128
const oauthTimeout = error?.message === timeoutMessage
107129

108130
return (
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Manifest } from 'webextension-polyfill'
2+
3+
export const CanRequestDynamically: Manifest.OptionalPermission[] = [
4+
'clipboardRead',
5+
'clipboardWrite',
6+
'notifications',
7+
'webRequestBlocking',
8+
]
9+
10+
export const XOAuthRequestOrigins: string[] = [
11+
// In order to send API request without CORS limit
12+
'https://api.twitter.com/*',
13+
// In order to run content script on it
14+
'https://firefly.social/api/mask/delegate-x-token',
15+
'https://firefly.social/api/auth/callback/twitter',
16+
'https://canary.firefly.social/api/mask/delegate-x-token',
17+
'https://canary.firefly.social/api/auth/callback/twitter',
18+
]

packages/shared-base/src/types/Routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,5 @@ export interface PopupRoutesParamsMap {
122122
from?: string | null
123123
}
124124
[PopupRoutes.Contacts]: { selectedToken: string | undefined }
125+
[PopupRoutes.CreateWallet]: { creatingFireflyWallet?: boolean }
125126
}

0 commit comments

Comments
 (0)