add `apps/connector` (#589)
* add `apps/connector` --------- Co-authored-by: Jakub Kotula <520927+jkbktl@users.noreply.github.com> Co-authored-by: Felicio Mununga <felicio@users.noreply.github.com> Co-authored-by: pavel <14926950+prichodko@users.noreply.github.com> Co-authored-by: marcelines <marcio@status.im>
|
@ -14,6 +14,12 @@ This monorepo contains packages for building web applications in the Status ecos
|
||||||
| [`@status-im/colors`](./packages/colors) | [![npm version](https://img.shields.io/npm/v/@status-im/colors.svg)](https://www.npmjs.com/package/@status-im/colors) | Auto-generated color palette based on our [design system](https://www.figma.com/design/v98g9ZiaSHYUdKWrbFg9eM/Foundations?node-id=619-5995&node-type=canvas&m=dev). |
|
| [`@status-im/colors`](./packages/colors) | [![npm version](https://img.shields.io/npm/v/@status-im/colors.svg)](https://www.npmjs.com/package/@status-im/colors) | Auto-generated color palette based on our [design system](https://www.figma.com/design/v98g9ZiaSHYUdKWrbFg9eM/Foundations?node-id=619-5995&node-type=canvas&m=dev). |
|
||||||
| [`@status-im/eslint-config`](./packages/eslint-config) | | Shared ESLint configuration for consistent code style across projects. |
|
| [`@status-im/eslint-config`](./packages/eslint-config) | | Shared ESLint configuration for consistent code style across projects. |
|
||||||
|
|
||||||
|
## Apps
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
| -------------------------------------- | ----------------------------------------------------------------------------- |
|
||||||
|
| [`./apps/connector`](./apps/connector) | Status Desktop Wallet extended to decentralised applications in your browser. |
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
Required:
|
Required:
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.ts', '*.tsx'],
|
||||||
|
extends: ['@status-im/eslint-config', 'plugin:tailwindcss/recommended'],
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'no-constant-binary-expression': 'error',
|
||||||
|
'no-restricted-globals': ['error', 'process'],
|
||||||
|
'jsx-a11y/alt-text': [
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
img: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// parser: 'esprima',
|
||||||
|
files: ['*.mjs'],
|
||||||
|
// env: {
|
||||||
|
// browser: true,
|
||||||
|
// es2021: true,
|
||||||
|
// },
|
||||||
|
// extends: ['eslint:recommended', 'plugin:import/recommended'],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['*.js'],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
#cache
|
||||||
|
.turbo
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
!.env.*
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# plasmo - https://www.plasmo.com
|
||||||
|
.plasmo
|
||||||
|
|
||||||
|
# bpp - http://bpp.browser.market/
|
||||||
|
keys.json
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
apps/
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"tailwindConfig": "./tailwind.config.ts",
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* @type {import('prettier').Options}
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
printWidth: 80,
|
||||||
|
tabWidth: 2,
|
||||||
|
useTabs: false,
|
||||||
|
semi: false,
|
||||||
|
singleQuote: false,
|
||||||
|
trailingComma: 'none',
|
||||||
|
bracketSpacing: true,
|
||||||
|
bracketSameLine: true,
|
||||||
|
plugins: ['@ianvs/prettier-plugin-sort-imports'],
|
||||||
|
importOrder: [
|
||||||
|
'<BUILTIN_MODULES>', // Node.js built-in modules
|
||||||
|
'<THIRD_PARTY_MODULES>', // Imports not matched by other special words or groups.
|
||||||
|
'', // Empty line
|
||||||
|
'^@plasmo/(.*)$',
|
||||||
|
'',
|
||||||
|
'^@plasmohq/(.*)$',
|
||||||
|
'',
|
||||||
|
'^~(.*)$',
|
||||||
|
'',
|
||||||
|
'^[./]',
|
||||||
|
],
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
#!/usr/bin/env groovy
|
||||||
|
library 'status-jenkins-lib@v1.9.1'
|
||||||
|
|
||||||
|
pipeline {
|
||||||
|
agent { label 'linux' }
|
||||||
|
|
||||||
|
options {
|
||||||
|
timestamps()
|
||||||
|
/* Prevent Jenkins jobs from running forever */
|
||||||
|
timeout(time: 10, unit: 'MINUTES')
|
||||||
|
/* manage how many builds we keep */
|
||||||
|
buildDiscarder(logRotator(
|
||||||
|
numToKeepStr: '20',
|
||||||
|
daysToKeepStr: '30',
|
||||||
|
))
|
||||||
|
disableConcurrentBuilds()
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
PLATFORM = 'chrome'
|
||||||
|
ZIP_NAME = utils.pkgFilename(
|
||||||
|
type: 'Extension',
|
||||||
|
version: 'none',
|
||||||
|
arch: 'chrome',
|
||||||
|
ext: 'zip',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Install') {
|
||||||
|
steps { script {
|
||||||
|
nix.shell('yarn install --frozen-lockfile', pure: false)
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Build') {
|
||||||
|
steps { script {
|
||||||
|
nix.shell('yarn build:chrome', pure: false)
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Zip') {
|
||||||
|
steps {
|
||||||
|
zip(
|
||||||
|
zipFile: env.ZIP_NAME,
|
||||||
|
dir: 'build/chrome-mv3-prod',
|
||||||
|
archive: false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Archive') {
|
||||||
|
steps {
|
||||||
|
archiveArtifacts(
|
||||||
|
artifacts: env.ZIP_NAME,
|
||||||
|
fingerprint: true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Upload') {
|
||||||
|
steps { script {
|
||||||
|
env.PKG_URL = s5cmd.upload(env.ZIP_NAME)
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
success { script { github.notifyPR(true) } }
|
||||||
|
failure { script { github.notifyPR(false) } }
|
||||||
|
cleanup { cleanWs() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,355 @@
|
||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
### 1. Definitions
|
||||||
|
|
||||||
|
**1.1. “Contributor”**
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
**1.2. “Contributor Version”**
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
**1.3. “Contribution”**
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
**1.4. “Covered Software”**
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
**1.5. “Incompatible With Secondary Licenses”**
|
||||||
|
means
|
||||||
|
|
||||||
|
* **(a)** that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
* **(b)** that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
**1.6. “Executable Form”**
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
**1.7. “Larger Work”**
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
**1.8. “License”**
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
**1.9. “Licensable”**
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
**1.10. “Modifications”**
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
* **(a)** any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
* **(b)** any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
**1.11. “Patent Claims” of a Contributor**
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
**1.12. “Secondary License”**
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
**1.13. “Source Code Form”**
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
**1.14. “You” (or “Your”)**
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, “You” includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, “control” means **(a)** the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or **(b)** ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
|
||||||
|
### 2. License Grants and Conditions
|
||||||
|
|
||||||
|
#### 2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
* **(a)** under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
* **(b)** under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
#### 2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
#### 2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
* **(a)** for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
* **(b)** for infringements caused by: **(i)** Your and any other third party's
|
||||||
|
modifications of Covered Software, or **(ii)** the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
* **(c)** under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
#### 2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
#### 2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
#### 2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
#### 2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
|
||||||
|
### 3. Responsibilities
|
||||||
|
|
||||||
|
#### 3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
#### 3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
* **(a)** such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
* **(b)** You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
#### 3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
#### 3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
#### 3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
|
||||||
|
### 4. Inability to Comply Due to Statute or Regulation
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: **(a)** comply with
|
||||||
|
the terms of this License to the maximum extent possible; and **(b)**
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
|
||||||
|
### 5. Termination
|
||||||
|
|
||||||
|
**5.1.** The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated **(a)** provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and **(b)** on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
**5.2.** If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
**5.3.** In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
|
||||||
|
### 6. Disclaimer of Warranty
|
||||||
|
|
||||||
|
> Covered Software is provided under this License on an “as is”
|
||||||
|
> basis, without warranty of any kind, either expressed, implied, or
|
||||||
|
> statutory, including, without limitation, warranties that the
|
||||||
|
> Covered Software is free of defects, merchantable, fit for a
|
||||||
|
> particular purpose or non-infringing. The entire risk as to the
|
||||||
|
> quality and performance of the Covered Software is with You.
|
||||||
|
> Should any Covered Software prove defective in any respect, You
|
||||||
|
> (not any Contributor) assume the cost of any necessary servicing,
|
||||||
|
> repair, or correction. This disclaimer of warranty constitutes an
|
||||||
|
> essential part of this License. No use of any Covered Software is
|
||||||
|
> authorized under this License except under this disclaimer.
|
||||||
|
|
||||||
|
### 7. Limitation of Liability
|
||||||
|
|
||||||
|
> Under no circumstances and under no legal theory, whether tort
|
||||||
|
> (including negligence), contract, or otherwise, shall any
|
||||||
|
> Contributor, or anyone who distributes Covered Software as
|
||||||
|
> permitted above, be liable to You for any direct, indirect,
|
||||||
|
> special, incidental, or consequential damages of any character
|
||||||
|
> including, without limitation, damages for lost profits, loss of
|
||||||
|
> goodwill, work stoppage, computer failure or malfunction, or any
|
||||||
|
> and all other commercial damages or losses, even if such party
|
||||||
|
> shall have been informed of the possibility of such damages. This
|
||||||
|
> limitation of liability shall not apply to liability for death or
|
||||||
|
> personal injury resulting from such party's negligence to the
|
||||||
|
> extent applicable law prohibits such limitation. Some
|
||||||
|
> jurisdictions do not allow the exclusion or limitation of
|
||||||
|
> incidental or consequential damages, so this exclusion and
|
||||||
|
> limitation may not apply to You.
|
||||||
|
|
||||||
|
|
||||||
|
### 8. Litigation
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
|
||||||
|
### 9. Miscellaneous
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
|
||||||
|
### 10. Versions of the License
|
||||||
|
|
||||||
|
#### 10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
#### 10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
#### 10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
#### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
## Exhibit A - Source Code Form License Notice
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
## Exhibit B - “Incompatible With Secondary Licenses” Notice
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
|
@ -0,0 +1,92 @@
|
||||||
|
# A wallet connector by Status
|
||||||
|
|
||||||
|
Status Desktop Wallet extended to decentralized applications in your browser.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
Depends on:
|
||||||
|
|
||||||
|
- Status Desktop https://github.com/status-im/status-desktop/tree/release/2.30.x
|
||||||
|
|
||||||
|
Tested with these browsers:
|
||||||
|
|
||||||
|
- Google Chrome
|
||||||
|
- Arc
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Google Chrome
|
||||||
|
|
||||||
|
#### Develop
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev:chrome
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build:chrome
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Load
|
||||||
|
|
||||||
|
Google Chrome > Window > Extensions > enable Developer mode
|
||||||
|
|
||||||
|
Google Chrome > Window > Extensions > Load unpacked > select build (build/chrome-mv3-dev)
|
||||||
|
|
||||||
|
> Note: Reloads automatically in development.
|
||||||
|
|
||||||
|
### Safari
|
||||||
|
|
||||||
|
#### Develop
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev:safari
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Convert
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcrun safari-web-extension-converter --no-open --macos-only --swift --project-location ./apps --app-name Status --bundle-identifier im.Status.Status ./build/safari-mv3-dev/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
xcodebuild -project apps/Status/Status.xcodeproj -scheme Status build
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Load
|
||||||
|
|
||||||
|
Safari > Settings... > Developer > Allow unsigned extensions
|
||||||
|
|
||||||
|
Safari > Settings... > Extensions > check Status
|
||||||
|
|
||||||
|
> Note: Does not reload automatically, requires build on change and converting as well if adding new files.
|
||||||
|
|
||||||
|
### Firefox Developer Edition
|
||||||
|
|
||||||
|
#### Develop
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn dev:firefox
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn build:firefox
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Load
|
||||||
|
|
||||||
|
https://support.mozilla.org/en-US/kb/add-on-signing-in-firefox#w_what-are-my-options-if-i-want-to-use-an-unsigned-add-on-advanced-users
|
||||||
|
|
||||||
|
Firefox Developer Edition > Tools > Add-ons and Themes > click on gear icon (Tools for all add-ons) > Install Add-on From File... > select build (build/firefox-mv3-prod.zip)
|
||||||
|
|
||||||
|
> Note: Does not reload automatically, requires build and load on change.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Download latest build from last merged PR or build from source. To use the extension see the load steps from [Development](#development) section.
|
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 91 KiB |
|
@ -0,0 +1,7 @@
|
||||||
|
import type { Provider } from './src/contents/provider'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
ethereum: Provider
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
{
|
||||||
|
"name": "connector",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"private": true,
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/status-im/status-web.git",
|
||||||
|
"directory": "apps/connector"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev:chrome": "plasmo dev --target=chrome-mv3",
|
||||||
|
"dev:safari": "plasmo dev --target=safari-mv3",
|
||||||
|
"dev:firefox": "plasmo dev --target=firefox-mv3",
|
||||||
|
"build:chrome": "plasmo build --target=chrome-mv3",
|
||||||
|
"build:firefox": "plasmo build --target=firefox-mv3 --zip",
|
||||||
|
"lint": "eslint ./src",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"package": "plasmo package",
|
||||||
|
"clean": "rimraf apps build .plasmo node_modules"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@plasmohq/messaging": "^0.6.2",
|
||||||
|
"@plasmohq/storage": "^1.11.0",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
|
"@radix-ui/react-switch": "^1.1.0",
|
||||||
|
"@status-im/colors": "^0.4.0",
|
||||||
|
"cva": "^1.0.0-beta.1",
|
||||||
|
"ethers": "^6.13.0",
|
||||||
|
"plasmo": "0.88.0",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"ts-pattern": "^5.2.0",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@ianvs/prettier-plugin-sort-imports": "4.1.1",
|
||||||
|
"@parcel/bundler-experimental": "^2.7.0",
|
||||||
|
"@status-im/eslint-config": "^0.3.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.13",
|
||||||
|
"@types/chrome": "0.0.258",
|
||||||
|
"@types/node": "20.11.5",
|
||||||
|
"@types/react": "18.2.48",
|
||||||
|
"@types/react-dom": "18.2.18",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
|
"husky": "^8.0.3",
|
||||||
|
"lint-staged": "^13.2.0",
|
||||||
|
"postcss": "^8.4.38",
|
||||||
|
"prettier": "3.2.4",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.1",
|
||||||
|
"rimraf": "^4.4.1",
|
||||||
|
"tailwind-merge": "^2.3.0",
|
||||||
|
"tailwind-scrollbar-utilities": "^0.2.0",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"typescript": "5.3.3"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "A wallet connector by Status BETA",
|
||||||
|
"description": "THIS EXTENSION IS FOR BETA TESTING. Status Desktop Wallet extended to decentralised applications in your browser.",
|
||||||
|
"host_permissions": [
|
||||||
|
"https://*/*",
|
||||||
|
"https://chromewebstore.google.com/*"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"storage"
|
||||||
|
],
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "no-reply@status.im"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{ts,tsx,js,jsx,mjs}": [
|
||||||
|
"eslint",
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"*.{yml,yaml,json}": [
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
'tailwindcss/nesting': {},
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
source ? builtins.fetchTarball {
|
||||||
|
url = "https://github.com/NixOS/nixpkgs/archive/614b4613980a522ba49f0d194531beddbb7220d3.tar.gz";
|
||||||
|
sha256 = "sha256:1kipdjdjcd1brm5a9lzlhffrgyid0byaqwfnpzlmw3q825z7nj6w";
|
||||||
|
},
|
||||||
|
pkgs ? import (source) {}
|
||||||
|
}:
|
||||||
|
|
||||||
|
pkgs.mkShell {
|
||||||
|
name = "browser-extension-shell";
|
||||||
|
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
nodejs_20
|
||||||
|
yarn
|
||||||
|
];
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Storage } from '@plasmohq/storage'
|
||||||
|
import iconConnected from 'url:../assets/icon-connected.png' // eslint-disable-line import/no-unresolved
|
||||||
|
import iconDisconnected from 'url:../assets/icon-disconnected.png' // eslint-disable-line import/no-unresolved
|
||||||
|
|
||||||
|
import { config } from '~config'
|
||||||
|
|
||||||
|
import type { ServiceWorkerMessage } from '~messages/service-worker-message'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the WebSocket server is reachable
|
||||||
|
*/
|
||||||
|
const storage = new Storage()
|
||||||
|
|
||||||
|
const DESKTOP_ENDPOINT_URL = config.desktop.rpc.url.replace('ws:', 'http:')
|
||||||
|
const CHECK_INTERVAL_MS = 5000
|
||||||
|
|
||||||
|
const isWebSocketServerReachable = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
await fetch(DESKTOP_ENDPOINT_URL, { method: 'HEAD' })
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkDesktopStatus() {
|
||||||
|
const isServerReachable = await isWebSocketServerReachable()
|
||||||
|
storage.set('status:desktop:running', isServerReachable)
|
||||||
|
chrome.action.setIcon({
|
||||||
|
path: isServerReachable ? iconConnected : iconDisconnected,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
checkDesktopStatus()
|
||||||
|
setInterval(checkDesktopStatus, CHECK_INTERVAL_MS)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for opening popup
|
||||||
|
*/
|
||||||
|
chrome.action.onClicked.addListener(async tab => {
|
||||||
|
if (!tab.id || !tab.url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.tabs.sendMessage(tab.id, {
|
||||||
|
type: 'status:icon:clicked',
|
||||||
|
} satisfies ServiceWorkerMessage)
|
||||||
|
})
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { useLocalStorage } from '~hooks/use-local-storage'
|
||||||
|
import { getFaviconUrl } from '~lib/get-favicon-url'
|
||||||
|
|
||||||
|
import { Network } from './network'
|
||||||
|
import { Switch } from './switch'
|
||||||
|
|
||||||
|
const Connected = () => {
|
||||||
|
const [defaultWallet, setDefaultWallet] = useLocalStorage(
|
||||||
|
'status:default-wallet',
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
const [, setReopen] = useLocalStorage('status:popup:reopen', false)
|
||||||
|
|
||||||
|
const dappUrl = window.location.hostname
|
||||||
|
const dappIcon = getFaviconUrl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-5 flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* fixme: set default favicon `onerror` */}
|
||||||
|
{dappIcon ? (
|
||||||
|
<img src={dappIcon} alt="icon" className="size-8 rounded-full" />
|
||||||
|
) : (
|
||||||
|
<div className="size-8 rounded-full bg-neutral-5" />
|
||||||
|
)}
|
||||||
|
<div className="text-27 font-semibold">{dappUrl}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Network />
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 rounded-2xl border border-neutral-20 bg-neutral-2.5 px-4 py-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-15 font-medium text-neutral-100">
|
||||||
|
Set as default wallet
|
||||||
|
</div>
|
||||||
|
<div className="text-13 font-regular text-neutral-50">
|
||||||
|
Launch Status (Desktop) when connecting to dApps with MetaMask or
|
||||||
|
other wallets
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={defaultWallet}
|
||||||
|
onCheckedChange={checked => {
|
||||||
|
setDefaultWallet(checked)
|
||||||
|
setReopen(true)
|
||||||
|
window.location.reload()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Connected }
|
|
@ -0,0 +1,80 @@
|
||||||
|
import {
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
Root,
|
||||||
|
Trigger,
|
||||||
|
} from '@radix-ui/react-dropdown-menu'
|
||||||
|
import { cx } from 'cva'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: [React.ReactElement, React.ReactElement]
|
||||||
|
modal?: boolean
|
||||||
|
onOpenChange?: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownMenu = (props: Props) => {
|
||||||
|
const { children, onOpenChange, modal } = props
|
||||||
|
|
||||||
|
const [trigger, content] = children
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Root onOpenChange={onOpenChange} modal={modal}>
|
||||||
|
<Trigger asChild>{trigger}</Trigger>
|
||||||
|
{content}
|
||||||
|
</Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DropdownMenuItemProps = {
|
||||||
|
label: string
|
||||||
|
onSelect: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const MenuItem = (props: DropdownMenuItemProps) => {
|
||||||
|
const { label, onSelect } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemBase onSelect={onSelect}>
|
||||||
|
<div className="flex items-center gap-8">
|
||||||
|
<p className="text-15 font-medium text-neutral-100">{label}</p>
|
||||||
|
</div>
|
||||||
|
</ItemBase>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Content = (props: React.ComponentProps<typeof DropdownMenuContent>) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuContent
|
||||||
|
{...props}
|
||||||
|
className={cx('w-64 p-1 rounded-xl bg-white-100', 'shadow-3 ')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ItemBase = (props: React.ComponentProps<typeof DropdownMenuItem>) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
{...props}
|
||||||
|
className="flex h-8 cursor-pointer select-none items-center justify-between gap-2 rounded-[10px] px-2 transition-colors hover:bg-neutral-5"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Separator = (
|
||||||
|
props: React.ComponentProps<typeof DropdownMenuSeparator>,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuSeparator
|
||||||
|
{...props}
|
||||||
|
className="mx-[-4px] my-1 h-px bg-neutral-10"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu.Content = Content
|
||||||
|
DropdownMenu.Item = MenuItem
|
||||||
|
DropdownMenu.Separator = Separator
|
||||||
|
|
||||||
|
export { DropdownMenu }
|
||||||
|
export type DropdownMenuProps = Omit<Props, 'children'>
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { cx } from 'cva'
|
||||||
|
|
||||||
|
import { config } from '~config'
|
||||||
|
|
||||||
|
import { DropdownMenu } from './dropdown-menu'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string
|
||||||
|
children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadUrl(dataurl: string) {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = dataurl
|
||||||
|
link.target = '_blank'
|
||||||
|
link.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const MacOsPicker = (props: Props) => {
|
||||||
|
const { children, className } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<button
|
||||||
|
className={cx(
|
||||||
|
'group flex items-center justify-center gap-1 whitespace-nowrap rounded-xl border text-center text-15 font-medium leading-normal outline-none transition-all',
|
||||||
|
'h-10 px-3',
|
||||||
|
'border-neutral-30 bg-white-100 text-neutral-100 hover:border-neutral-40 active:border-neutral-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="-mt-px">
|
||||||
|
<svg
|
||||||
|
color="rgba(9 16 28 / 100%)"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
focusable="false"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="rgba(9 16 28 / 100%)"
|
||||||
|
d="M14.357 10c.023 2.422 2.12 3.227 2.143 3.238-.018.056-.335 1.148-1.105 2.275-.665.975-1.356 1.946-2.444 1.966-1.069.02-1.413-.635-2.635-.635-1.222 0-1.604.615-2.616.655-1.05.04-1.85-1.054-2.52-2.025-1.371-1.987-2.42-5.613-1.012-8.062.699-1.215 1.948-1.985 3.303-2.005 1.031-.02 2.005.695 2.635.695.63 0 1.813-.86 3.056-.733.521.022 1.982.21 2.92 1.587-.075.047-1.743 1.02-1.725 3.044Zm-2.009-5.945c.558-.677.933-1.618.83-2.555-.803.032-1.775.537-2.351 1.213-.517.598-.97 1.556-.847 2.475.895.069 1.81-.457 2.368-1.133Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="none"
|
||||||
|
className="-ml-1 transition-transform duration-200 group-aria-expanded:rotate-180"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M3 10C3 6.13401 6.13401 3 10 3C13.866 3 17 6.13401 17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10Z"
|
||||||
|
fill="#E7EAEE"
|
||||||
|
fillOpacity={1}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M7 8.5L10 11.5L13 8.5"
|
||||||
|
stroke={'#09101C'}
|
||||||
|
strokeWidth="1.2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<DropdownMenu.Content
|
||||||
|
align="start"
|
||||||
|
sideOffset={8}
|
||||||
|
className="[&>div]:outline-none"
|
||||||
|
>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
label="Apple Silicon"
|
||||||
|
onSelect={() =>
|
||||||
|
downloadUrl(config.desktop.downloadUrls.macos.silicon)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
label="Intel"
|
||||||
|
onSelect={() => downloadUrl(config.desktop.downloadUrls.macos.intel)}
|
||||||
|
/>
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MacOsPicker }
|
|
@ -0,0 +1,39 @@
|
||||||
|
import arbitrumImage from 'url:../../assets/network/arbitrum.webp'
|
||||||
|
import mainnetImage from 'url:../../assets/network/mainnet.webp'
|
||||||
|
import optimismImage from 'url:../../assets/network/optimism.webp'
|
||||||
|
|
||||||
|
const Network = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex h-[64px] items-center rounded-2xl border border-neutral-10 bg-white-100 px-4">
|
||||||
|
<div className="flex w-full items-center">
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<div className="text-13 font-regular text-neutral-50">
|
||||||
|
Supported networks
|
||||||
|
</div>
|
||||||
|
<div className="text-15 font-medium text-neutral-100">
|
||||||
|
Mainnet, Optimism, Arbitrum
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<img
|
||||||
|
className="box-content size-6 rounded-full border border-white-100"
|
||||||
|
src={mainnetImage}
|
||||||
|
alt="Mainnet"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
className="ml-[-5px] box-content size-6 rounded-full border border-white-100"
|
||||||
|
src={optimismImage}
|
||||||
|
alt="Optimism"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
className="ml-[-5px] box-content size-6 rounded-full border border-white-100"
|
||||||
|
src={arbitrumImage}
|
||||||
|
alt="Arbitrum"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Network }
|
|
@ -0,0 +1,21 @@
|
||||||
|
import unableToConnectImage from 'url:../../assets/unable-to-connect.png'
|
||||||
|
|
||||||
|
import { DownloadButton } from './download-button'
|
||||||
|
|
||||||
|
const NotConnected = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<img src={unableToConnectImage} alt="Not connected" className="w-full" />
|
||||||
|
<div className="max-w-[304px] pb-5 pt-4 text-center">
|
||||||
|
<p className="text-19 font-semibold">Unable to connect to Status</p>
|
||||||
|
<p className="text-15">
|
||||||
|
Make sure Status desktop app is running on your machine or download it
|
||||||
|
below.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<DownloadButton />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { NotConnected }
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { useStorage } from '@plasmohq/storage/hook'
|
||||||
|
|
||||||
|
const PinInstructions = () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const [_showPinInstructions, setShowPinInstructions] = useStorage(
|
||||||
|
'show-pin-instructions',
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="-mx-5 mt-8 flex items-center gap-2 border-t border-neutral-80/5 px-4 pt-3">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-15 font-semibold text-neutral-100">
|
||||||
|
Pin the extension
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-13 font-regular">
|
||||||
|
Click <PuzzleIcon /> and then <PinIcon /> and voilà!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="group inline-flex h-8 items-center justify-center gap-1 whitespace-nowrap rounded-xl border border-neutral-30 bg-white-100 px-3 text-center text-15 font-medium leading-normal text-neutral-100 outline-none transition-all hover:border-neutral-40 active:border-neutral-50"
|
||||||
|
onClick={() => setShowPinInstructions(false)}
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PinInstructions }
|
||||||
|
|
||||||
|
const PuzzleIcon = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none">
|
||||||
|
<path
|
||||||
|
fill="#09101C"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M7.5 2.6a.9.9 0 0 0-.9.9v.6H5V2.9h.487a2.1 2.1 0 0 1 4.026 0h.611c.4 0 .735 0 1.01.023.287.023.56.074.82.206a2.1 2.1 0 0 1 .917.918c.132.26.183.532.207.82.022.274.022.61.022 1.01v.61a2.1 2.1 0 0 1 0 4.026v.612c0 .4 0 .735-.022 1.01-.024.287-.075.56-.207.819a2.1 2.1 0 0 1-.918.918c-.259.132-.532.183-.819.206-.275.023-.61.023-1.01.023H4.876c-.4 0-.735 0-1.01-.023-.287-.023-.56-.074-.82-.206a2.1 2.1 0 0 1-.917-.918c-.132-.26-.183-.532-.207-.82-.022-.274-.022-.61-.022-1.01V9.4h.6a.9.9 0 1 0 0-1.8h-.6V5.876c0-.4 0-.735.022-1.01.024-.287.075-.56.207-.819a2.1 2.1 0 0 1 .918-.918c.259-.132.532-.183.819-.206.275-.022.61-.022 1.01-.022H5v1.2h-.1c-.43 0-.716 0-.936.018-.213.017-.31.048-.373.08a.9.9 0 0 0-.393.393c-.031.062-.062.16-.08.372-.018.22-.018.507-.018.937v.586a2.1 2.1 0 0 1 0 4.026v.588c0 .43 0 .716.018.936.018.213.049.31.08.372a.9.9 0 0 0 .393.393c.062.032.16.063.373.08.22.018.506.018.936.018h5.2c.43 0 .716 0 .936-.018.213-.017.31-.048.373-.08a.9.9 0 0 0 .393-.393c.031-.062.062-.16.08-.372.017-.22.018-.507.018-.937V9.4h.6a.9.9 0 1 0 0-1.8h-.6V5.9c0-.43 0-.716-.018-.936-.018-.213-.049-.31-.08-.372a.9.9 0 0 0-.393-.393c-.062-.032-.16-.063-.373-.08-.22-.018-.506-.018-.936-.018H8.4v-.6a.9.9 0 0 0-.9-.9Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const PinIcon = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none">
|
||||||
|
<g clipPath="url(#a)">
|
||||||
|
<path
|
||||||
|
fill="#09101C"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M6.657 5.955c.497-.86.897-1.491 1.25-1.942.355-.454.625-.677.846-.78.377-.176.821-.097 1.932.545 1.111.641 1.402.986 1.439 1.4.02.243-.037.59-.253 1.124-.214.53-.56 1.193-1.057 2.053l-.073.126-.007.145-.173 3.53L3.58 8.123l2.896-1.973.113-.077.069-.119Zm4.628-3.217c-1.054-.608-2.033-1.062-3.04-.593-.47.22-.88.613-1.284 1.129-.387.495-.8 1.147-1.274 1.962L2.114 7.67l-.789.538.827.478 3.744 2.161-1.886 3.114h1.5l1.438-2.506 3.864 2.23.852.492.048-.981.214-4.367c.467-.815.823-1.497 1.058-2.078.245-.607.38-1.16.335-1.677-.097-1.105-.98-1.727-2.034-2.336Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="a">
|
||||||
|
<path fill="#fff" d="M0 0h16v16H0z" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
)
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { cva } from 'cva'
|
||||||
|
import { match } from 'ts-pattern'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
status: 'on' | 'off'
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = cva({
|
||||||
|
base: 'flex h-6 items-center gap-1 rounded-[20px] border px-2 text-13',
|
||||||
|
variants: {
|
||||||
|
status: {
|
||||||
|
on: 'border-success-/20 bg-success-/10 text-success-60',
|
||||||
|
off: 'border-danger-/20 bg-danger-/10 text-danger-60',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const StatusTag = (props: Props) => {
|
||||||
|
const { status } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles({ status })}>
|
||||||
|
{match(status)
|
||||||
|
.with('on', () => (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
viewBox="0 0 12 12"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8 1.5l-1.5 3H10L4.5 10 5 7H2l2.5-5.5H8z"
|
||||||
|
stroke="#1C8A80"
|
||||||
|
strokeWidth={1.1}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="text-success-60">Status desktop running</span>
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
.with('off', () => (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
viewBox="0 0 12 12"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M5.45 1v4.5h1.1V1h-1.1zm2.525 1.58a3.95 3.95 0 11-3.95 0l-.55-.954a5.05 5.05 0 105.05 0l-.55.953z"
|
||||||
|
fill="#BA434D"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="text-danger-60">Status desktop not running</span>
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
.exhaustive()}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { StatusTag }
|
|
@ -0,0 +1,20 @@
|
||||||
|
import * as BaseSwitch from '@radix-ui/react-switch'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
checked: boolean
|
||||||
|
onCheckedChange: (checked: boolean) => void
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Switch = (props: Props) => {
|
||||||
|
return (
|
||||||
|
<BaseSwitch.Root
|
||||||
|
className="relative h-[20px] w-[30px] cursor-default rounded-full bg-neutral-30 outline-none data-[disabled]:cursor-not-allowed data-[state=checked]:bg-customisation-blue-50 data-[disabled]:opacity-30"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<BaseSwitch.Thumb className="block size-[16px] translate-x-0.5 rounded-full bg-white-100 transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[12px]" />
|
||||||
|
</BaseSwitch.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Switch }
|
|
@ -0,0 +1,17 @@
|
||||||
|
export const config = {
|
||||||
|
desktop: {
|
||||||
|
rpc: {
|
||||||
|
url: 'ws://localhost:8586',
|
||||||
|
method: 'connector_callRPC',
|
||||||
|
},
|
||||||
|
downloadUrls: {
|
||||||
|
macos: {
|
||||||
|
silicon:
|
||||||
|
'https://status.app/api/download/macos-silicon?source=connector',
|
||||||
|
intel: 'https://status.app/api/download/macos-intel?source=connector',
|
||||||
|
},
|
||||||
|
windows: 'https://status.app/api/download/windows?source=connector',
|
||||||
|
linux: 'https://status.app/api/download/linux?source=connector',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url(data-base64:../../assets/fonts/Inter.woff2) format('woff2');
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { useStorage } from '@plasmohq/storage/hook'
|
||||||
|
import styleText from 'data-text:../style.css' // eslint-disable-line import/no-unresolved
|
||||||
|
import logoSrc from 'url:../../assets/logo.png'
|
||||||
|
|
||||||
|
import { Connected } from '~components/connected'
|
||||||
|
import { NotConnected } from '~components/not-connected'
|
||||||
|
import { PinInstructions } from '~components/pin-instructions'
|
||||||
|
import { StatusTag } from '~components/status-tag'
|
||||||
|
import { useLocalStorage } from '~hooks/use-local-storage'
|
||||||
|
import { useOutsideClick } from '~hooks/use-outside-click'
|
||||||
|
import { ServiceWorkerMessage } from '~messages/service-worker-message'
|
||||||
|
|
||||||
|
import type { PlasmoGetStyle } from 'plasmo'
|
||||||
|
|
||||||
|
export const getStyle: PlasmoGetStyle = () => {
|
||||||
|
const style = document.createElement('style')
|
||||||
|
style.textContent = styleText
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageHandler = Parameters<typeof chrome.runtime.onMessage.addListener>[0]
|
||||||
|
|
||||||
|
export default function Popup() {
|
||||||
|
const [connected] = useStorage('status:desktop:running')
|
||||||
|
const [showPinInstructions] = useStorage('show-pin-instructions', true)
|
||||||
|
|
||||||
|
const [reopen, setReopen] = useLocalStorage('status:popup:reopen', false)
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const handleOutsideClick = useCallback(() => setOpen(false), [])
|
||||||
|
|
||||||
|
useOutsideClick(containerRef, handleOutsideClick)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMessage: MessageHandler = (message: ServiceWorkerMessage) => {
|
||||||
|
try {
|
||||||
|
message = ServiceWorkerMessage.parse(message)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === 'status:icon:clicked') {
|
||||||
|
setOpen(open => !open)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener(handleMessage)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
chrome.runtime.onMessage.removeListener(handleMessage)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (reopen) {
|
||||||
|
setOpen(true)
|
||||||
|
setReopen(false)
|
||||||
|
}
|
||||||
|
}, [reopen, setReopen])
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="fixed right-2 top-2 max-w-[390px] overflow-hidden rounded-3xl"
|
||||||
|
>
|
||||||
|
<div className="grid w-full grid-rows-[auto,1fr] rounded-3xl bg-neutral-100">
|
||||||
|
{/* Bar */}
|
||||||
|
<div className="flex items-center justify-between px-5 py-3">
|
||||||
|
<img src={logoSrc} alt="Status" className="size-8" />
|
||||||
|
<StatusTag status={connected ? 'on' : 'off'} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-1 mb-1 overflow-hidden rounded-[20px] bg-white-100 p-5">
|
||||||
|
{connected ? <Connected /> : <NotConnected />}
|
||||||
|
{showPinInstructions && <PinInstructions />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,319 @@
|
||||||
|
import { ProviderRpcError } from '~lib/provider-rpc-error'
|
||||||
|
import { RequestArguments } from '~lib/request-arguments'
|
||||||
|
import { ProxyMessage } from '~messages/proxy-message'
|
||||||
|
|
||||||
|
import type { ProviderMessage } from '~messages/provider-message'
|
||||||
|
import type { PlasmoCSConfig } from 'plasmo'
|
||||||
|
|
||||||
|
export const config: PlasmoCSConfig = {
|
||||||
|
matches: ['https://*/*'],
|
||||||
|
world: 'MAIN',
|
||||||
|
run_at: 'document_start',
|
||||||
|
all_frames: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event =
|
||||||
|
| 'connect'
|
||||||
|
| 'connected'
|
||||||
|
| 'disconnect'
|
||||||
|
| 'close'
|
||||||
|
| 'error'
|
||||||
|
| 'chainChanged'
|
||||||
|
| 'accountsChanged'
|
||||||
|
| 'networkChanged'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://eips.ethereum.org/EIPS/eip-1193 for spec
|
||||||
|
*/
|
||||||
|
export class Provider {
|
||||||
|
#listeners: Map<Event, (...args: unknown[]) => void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://github.com/snapshot-labs/lock/blob/503f4b07f1b631b1eed0dca993110dc561189261/src/utils.ts for other examples
|
||||||
|
*/
|
||||||
|
public isStatus: boolean
|
||||||
|
|
||||||
|
public isMetaMask: boolean
|
||||||
|
public _metamask: {
|
||||||
|
isUnlocked: () => Promise<true>
|
||||||
|
} | null
|
||||||
|
|
||||||
|
// public isCoinbaseWallet: boolean
|
||||||
|
public qrUrl: null
|
||||||
|
|
||||||
|
public autoRefreshOnNetworkChange: boolean
|
||||||
|
|
||||||
|
public provider: Record<string, unknown> | null
|
||||||
|
|
||||||
|
public __isProvider: boolean
|
||||||
|
|
||||||
|
public connected: boolean
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.isStatus = true
|
||||||
|
this.isMetaMask = false
|
||||||
|
this._metamask = null
|
||||||
|
// this.isCoinbaseWallet = false
|
||||||
|
this.qrUrl = null
|
||||||
|
this.autoRefreshOnNetworkChange = false
|
||||||
|
this.provider = null
|
||||||
|
this.__isProvider = false
|
||||||
|
this.connected = false
|
||||||
|
this.#listeners = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
public async request(args: RequestArguments) {
|
||||||
|
try {
|
||||||
|
args = RequestArguments.parse(args)
|
||||||
|
} catch {
|
||||||
|
throw new ProviderRpcError({
|
||||||
|
code: -32602,
|
||||||
|
message: 'Invalid request arguments',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitUntilComplete(document)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const { method, params } = args
|
||||||
|
|
||||||
|
const messageChannel = new MessageChannel()
|
||||||
|
|
||||||
|
messageChannel.port1.onmessage = ({ data }) => {
|
||||||
|
try {
|
||||||
|
const message = ProxyMessage.parse(data)
|
||||||
|
|
||||||
|
messageChannel.port1.close()
|
||||||
|
|
||||||
|
// note: method side-effects
|
||||||
|
switch (message.type) {
|
||||||
|
case 'status:proxy:success': {
|
||||||
|
if (
|
||||||
|
(method === 'eth_requestAccounts' ||
|
||||||
|
method === 'eth_accounts') &&
|
||||||
|
!this.connected
|
||||||
|
) {
|
||||||
|
this.#listeners.get('connect')?.({ chainId: '0x1' })
|
||||||
|
this.#listeners.get('connected')?.({ chainId: '0x1' })
|
||||||
|
this.connected = true
|
||||||
|
|
||||||
|
console.log('connected::')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (method === 'wallet_switchEthereumChain') {
|
||||||
|
this.#listeners.get('chainChanged')?.(message.data)
|
||||||
|
this.#listeners.get('networkChanged')?.(message.data)
|
||||||
|
|
||||||
|
console.log('chainChanged::')
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(message.data)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case 'status:proxy:error': {
|
||||||
|
console.error(message.error)
|
||||||
|
|
||||||
|
// note: for those dApps that make call after having permissions revoked
|
||||||
|
if (
|
||||||
|
message.error.message === 'dApp is not permitted by user' &&
|
||||||
|
this.connected
|
||||||
|
) {
|
||||||
|
this.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(new ProviderRpcError(message.error))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// we don't reject here because incoming message is not from the proxy
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerMessage: ProviderMessage = {
|
||||||
|
type: 'status:provider',
|
||||||
|
data: {
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
window.postMessage(providerMessage, window.origin, [messageChannel.port2])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
|
public async send(...args: unknown[]): Promise<unknown> {
|
||||||
|
return await this.request({
|
||||||
|
method: args[0] as string,
|
||||||
|
params: args[1] as Record<string, unknown>,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public on(event: Event, handler: (args: unknown) => void): void {
|
||||||
|
console.log('on::', event, handler)
|
||||||
|
|
||||||
|
this.#listeners.set(event, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
|
public async close(...args: unknown[]): Promise<void> {
|
||||||
|
console.log('close::', args)
|
||||||
|
|
||||||
|
this.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeListener(event: Event, handler: (args: unknown) => void): void {
|
||||||
|
console.log('removeListener::', event, handler)
|
||||||
|
|
||||||
|
// note: not all dapps remove these on disconnect
|
||||||
|
if (event === 'close' || event === 'disconnect') {
|
||||||
|
this.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#listeners.delete(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async disconnect() {
|
||||||
|
if (!this.connected) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connected = false
|
||||||
|
|
||||||
|
console.log('disconnect::')
|
||||||
|
|
||||||
|
await this.request({
|
||||||
|
method: 'wallet_revokePermissions',
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
eth_accounts: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
type: 'status:provider:disconnect',
|
||||||
|
} satisfies ProviderMessage,
|
||||||
|
window.origin,
|
||||||
|
)
|
||||||
|
|
||||||
|
this.#listeners.get('disconnect')?.()
|
||||||
|
this.#listeners.get('close')?.()
|
||||||
|
this.#listeners.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new Provider()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://eips.ethereum.org/EIPS/eip-6963 for spec
|
||||||
|
*/
|
||||||
|
function announceProvider() {
|
||||||
|
const info = {
|
||||||
|
uuid: 'c14d6a7e-14c2-477d-bcb7-ffb732145eae',
|
||||||
|
name: 'Status',
|
||||||
|
icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzk1MF8xMjM5NikiPiA8bWFzayBpZD0ibWFzazBfOTUwXzEyMzk2IiBzdHlsZT0ibWFzay10eXBlOmFscGhhIiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSIwIiB5PSItMSIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMzIj4gPHBhdGggZD0iTTE2IC0wLjAwMDQ4ODI4MUM0IC0wLjAwMDQ4ODI4MSAwIDMuOTk5NTEgMCAxNS45OTk1QzAgMjcuOTk5NSA0IDMxLjk5OTUgMTYgMzEuOTk5NUMyOCAzMS45OTk1IDMyIDI3Ljk5OTUgMzIgMTUuOTk5NUMzMiAzLjk5OTUxIDI4IC0wLjAwMDQ4ODI4MSAxNiAtMC4wMDA0ODgyODFaIiBmaWxsPSJ3aGl0ZSIvPiA8L21hc2s+IDxnIG1hc2s9InVybCgjbWFzazBfOTUwXzEyMzk2KSI+IDxnIGZpbHRlcj0idXJsKCNmaWx0ZXIwX2ZfOTUwXzEyMzk2KSI+IDxjaXJjbGUgY3g9IjIzIiBjeT0iOC45OTk1MSIgcj0iMTkiIGZpbGw9IiMxOTkyRDciLz4gPC9nPiA8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMV9mXzk1MF8xMjM5NikiPiA8Y2lyY2xlIGN4PSIzMyIgY3k9IjE4Ljk5OTUiIHI9IjE5IiBmaWxsPSIjRjZCMDNDIi8+IDwvZz4gPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjJfZl85NTBfMTIzOTYpIj4gPGNpcmNsZSBjeD0iNSIgY3k9IjMwLjk5OTUiIHI9IjE5IiBmaWxsPSIjRkY3RDQ2Ii8+IDwvZz4gPGcgZmlsdGVyPSJ1cmwoI2ZpbHRlcjNfZl85NTBfMTIzOTYpIj4gPGNpcmNsZSBjeD0iLTciIGN5PSI4Ljk5OTUxIiByPSIxOSIgZmlsbD0iIzcxNDBGRCIvPiA8L2c+IDxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTguMTI4NCA4LjgwODQzQzE0Ljk1NTEgOC45ODk4NCAxMi42MTIxIDExLjg3NDMgMTIuMzUxNiAxNS4xMzc1QzEyLjM2NzYgMTUuMTMyNiAxMi4zODUzIDE1LjEyNzYgMTIuNDAzIDE1LjEyMjdDMTIuNDIwNyAxNS4xMTc4IDEyLjQzODQgMTUuMTEyOSAxMi40NTQ0IDE1LjEwOEMxMi44NTM1IDE1LjAwNjcgMTMuMjYxNyAxNC45NDUyIDEzLjY3MzEgMTQuOTI0M0MxNC41NjQxIDE0Ljg3NDUgMTUuMjg4NyAxNC45NTE2IDE2LjAxMzIgMTUuMDI4OEMxNi43Mzg3IDE1LjEwNiAxNy40NjQgMTUuMTgzMiAxOC4zNTYyIDE1LjEzMjlDMTguNzY1OCAxNS4xMTEzIDE5LjE3MjIgMTUuMDQ3OSAxOS41Njg4IDE0Ljk0NEMyMS4yNDc2IDE0LjUwODYgMjIuMjEzNSAxMy4zNzQgMjIuMTI4MiAxMS44MTkxQzIyLjAyMzEgOS44ODcwOCAyMC4wNzY5IDguNjk3MzIgMTguMTI4NCA4LjgwODQzWk0xMy44Nzk2IDIzLjE5MDlDMTcuMDUyOSAyMy4wMDk1IDE5LjM5NTkgMjAuMTI1IDE5LjY1NjQgMTYuODYxOEMxOS42MzYzIDE2Ljg2OCAxOS42MTM1IDE2Ljg3NDIgMTkuNTkxNSAxNi44ODAxQzE5LjU3ODQgMTYuODgzNyAxOS41NjU1IDE2Ljg4NzIgMTkuNTUzNiAxNi44OTA1QzE5LjE1NDUgMTYuOTkxOSAxOC43NDYyIDE3LjA1MzQgMTguMzM0OCAxNy4wNzQyQzE3LjQ0MjcgMTcuMTI0OSAxNi43MTczIDE3LjA0NzkgMTUuOTkxOSAxNi45NzA4QzE1LjI2NzQgMTYuODkzOCAxNC41NDI4IDE2LjgxNjkgMTMuNjUxOCAxNi44NjcxQzEzLjI0MjEgMTYuODg4OCAxMi44MzU4IDE2Ljk1MjEgMTIuNDM5MiAxNy4wNTYxQzEwLjc2MDMgMTcuNDkwNyA5Ljc5ODI5IDE4LjYyNTMgOS44Nzk3OSAyMC4xODAyQzkuOTg0OTEgMjIuMTEyMiAxMS45MzExIDIzLjMwMiAxMy44Nzk2IDIzLjE5MDlaIiBmaWxsPSJ3aGl0ZSIvPiA8L2c+IDwvZz4gPGRlZnM+IDxmaWx0ZXIgaWQ9ImZpbHRlcjBfZl85NTBfMTIzOTYiIHg9Ii01LjQzMDI2IiB5PSItMTkuNDMwNyIgd2lkdGg9IjU2Ljg2MDUiIGhlaWdodD0iNTYuODYwNSIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPiA8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPiA8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPiA8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0LjcxNTEzIiByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfOTUwXzEyMzk2Ii8+IDwvZmlsdGVyPiA8ZmlsdGVyIGlkPSJmaWx0ZXIxX2ZfOTUwXzEyMzk2IiB4PSI0LjU2OTc0IiB5PSItOS40MzA3NSIgd2lkdGg9IjU2Ljg2MDUiIGhlaWdodD0iNTYuODYwNSIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPiA8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPiA8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPiA8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0LjcxNTEzIiByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfOTUwXzEyMzk2Ii8+IDwvZmlsdGVyPiA8ZmlsdGVyIGlkPSJmaWx0ZXIyX2ZfOTUwXzEyMzk2IiB4PSItMjMuNDMwMyIgeT0iMi41NjkyNSIgd2lkdGg9IjU2Ljg2MDUiIGhlaWdodD0iNTYuODYwNSIgZmlsdGVyVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM9InNSR0IiPiA8ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPiA8ZmVCbGVuZCBtb2RlPSJub3JtYWwiIGluPSJTb3VyY2VHcmFwaGljIiBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJzaGFwZSIvPiA8ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0LjcxNTEzIiByZXN1bHQ9ImVmZmVjdDFfZm9yZWdyb3VuZEJsdXJfOTUwXzEyMzk2Ii8+IDwvZmlsdGVyPiA8ZmlsdGVyIGlkPSJmaWx0ZXIzX2ZfOTUwXzEyMzk2IiB4PSItMzUuNDMwMyIgeT0iLTE5LjQzMDciIHdpZHRoPSI1Ni44NjA1IiBoZWlnaHQ9IjU2Ljg2MDUiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4gPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4gPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4gPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iNC43MTUxMyIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzk1MF8xMjM5NiIvPiA8L2ZpbHRlcj4gPGNsaXBQYXRoIGlkPSJjbGlwMF85NTBfMTIzOTYiPiA8cmVjdCB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9IndoaXRlIi8+IDwvY2xpcFBhdGg+IDwvZGVmcz4gPC9zdmc+',
|
||||||
|
rdns: 'app.status',
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('eip6963:announceProvider', {
|
||||||
|
detail: Object.freeze({ info, provider }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('eip6963:requestProvider', () => {
|
||||||
|
announceProvider()
|
||||||
|
})
|
||||||
|
|
||||||
|
announceProvider()
|
||||||
|
|
||||||
|
function injectProvider() {
|
||||||
|
Object.defineProperties(provider, {
|
||||||
|
isStatus: {
|
||||||
|
value: true,
|
||||||
|
writable: false,
|
||||||
|
},
|
||||||
|
isMetaMask: {
|
||||||
|
value: true,
|
||||||
|
writable: false,
|
||||||
|
},
|
||||||
|
_metamask: {
|
||||||
|
value: {
|
||||||
|
isUnlocked: () => new Promise(resolve => resolve(true)),
|
||||||
|
},
|
||||||
|
writable: false,
|
||||||
|
},
|
||||||
|
// isCoinbaseWallet: {
|
||||||
|
// value: true,
|
||||||
|
// writable: false,
|
||||||
|
// },
|
||||||
|
qrUrl: {
|
||||||
|
value: null,
|
||||||
|
writable: false,
|
||||||
|
},
|
||||||
|
autoRefreshOnNetworkChange: {
|
||||||
|
value: false,
|
||||||
|
writable: true,
|
||||||
|
},
|
||||||
|
provider: {
|
||||||
|
value: {},
|
||||||
|
writable: true,
|
||||||
|
},
|
||||||
|
['__isProvider']: {
|
||||||
|
value: true,
|
||||||
|
writable: false,
|
||||||
|
},
|
||||||
|
connected: {
|
||||||
|
value: true,
|
||||||
|
writable: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.seal(provider)
|
||||||
|
|
||||||
|
Object.defineProperties(window, {
|
||||||
|
ethereum: {
|
||||||
|
get() {
|
||||||
|
return provider
|
||||||
|
},
|
||||||
|
configurable: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// let redefinedProvider: any
|
||||||
|
|
||||||
|
if (window.localStorage.getItem('status:default-wallet') !== 'false') {
|
||||||
|
// redefinedProvider = window.ethereum
|
||||||
|
|
||||||
|
injectProvider()
|
||||||
|
}
|
||||||
|
|
||||||
|
// window.addEventListener('storage', () => {
|
||||||
|
// if (window.localStorage.getItem('status:default-wallet') === 'false') {
|
||||||
|
// window.ethereum = redefinedProvider
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
async function waitUntilComplete(document: Document): Promise<void> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
resolve()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('readystatechange', () => {
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { getFaviconUrl } from '~lib/get-favicon-url'
|
||||||
|
import { ProviderMessage } from '~messages/provider-message'
|
||||||
|
|
||||||
|
import { DesktopClient } from '../lib/desktop-client'
|
||||||
|
|
||||||
|
import type { ProxyMessage } from '~messages/proxy-message'
|
||||||
|
import type { EthersError } from 'ethers'
|
||||||
|
import type { PlasmoCSConfig } from 'plasmo'
|
||||||
|
|
||||||
|
export const config: PlasmoCSConfig = {
|
||||||
|
run_at: 'document_start',
|
||||||
|
}
|
||||||
|
|
||||||
|
const desktopClient = new DesktopClient()
|
||||||
|
|
||||||
|
const handleMessage = async (event: MessageEvent) => {
|
||||||
|
if (event.origin !== window.origin) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let message: ProviderMessage
|
||||||
|
try {
|
||||||
|
message = ProviderMessage.parse(event.data)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === 'status:provider:disconnect') {
|
||||||
|
desktopClient.stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type !== 'status:provider') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.ports.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('request::', message.data)
|
||||||
|
|
||||||
|
const response = await desktopClient.send({
|
||||||
|
...message.data,
|
||||||
|
name: window.location.hostname,
|
||||||
|
url: window.origin,
|
||||||
|
iconUrl: getFaviconUrl() ?? '',
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('response::', response)
|
||||||
|
|
||||||
|
event.ports[0].postMessage({
|
||||||
|
type: 'status:proxy:success',
|
||||||
|
data: response,
|
||||||
|
} satisfies ProxyMessage)
|
||||||
|
} catch (error) {
|
||||||
|
let proxyError = {
|
||||||
|
code: -32603,
|
||||||
|
message: isError(error) ? error.message : 'Internal error',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ethers.js library has a custom error detection mechanism.
|
||||||
|
* - Detected errors are stored in the `info` object:
|
||||||
|
* @see https://github.com/ethers-io/ethers.js/blob/72c2182d01afa855d131e82635dca3da063cfb31/src.ts/providers/provider-jsonrpc.ts#L976-L1057
|
||||||
|
* - Undetected errors are stored in the `error` field:
|
||||||
|
* @see https://github.com/ethers-io/ethers.js/blob/72c2182d01afa855d131e82635dca3da063cfb31/src.ts/providers/provider-jsonrpc.ts#L1059
|
||||||
|
*/
|
||||||
|
if (isEthersError(error)) {
|
||||||
|
if (isRpcError(error.error)) {
|
||||||
|
proxyError = error.error
|
||||||
|
} else if (isRpcError(error.info?.error)) {
|
||||||
|
proxyError = error.info.error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyMessage: ProxyMessage = {
|
||||||
|
type: 'status:proxy:error',
|
||||||
|
error: proxyError,
|
||||||
|
}
|
||||||
|
|
||||||
|
event.ports[0].postMessage(proxyMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isError(error: unknown): error is Error {
|
||||||
|
return !!error && typeof error === 'object' && 'message' in error
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEthersError(error: unknown): error is EthersError & {
|
||||||
|
info?: { error?: { code: number; message: string } }
|
||||||
|
} {
|
||||||
|
return (
|
||||||
|
!!error &&
|
||||||
|
typeof error === 'object' &&
|
||||||
|
'error' in error &&
|
||||||
|
error.error !== null &&
|
||||||
|
typeof error.error === 'object' &&
|
||||||
|
'code' in error.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRpcError(
|
||||||
|
error: unknown,
|
||||||
|
): error is { code: number; message: string } {
|
||||||
|
return (
|
||||||
|
!!error &&
|
||||||
|
typeof error === 'object' &&
|
||||||
|
'code' in error &&
|
||||||
|
'message' in error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', handleMessage)
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Navigator {
|
||||||
|
// @see https://github.com/lukewarlow/user-agent-data-types/blob/master/index.d.ts
|
||||||
|
userAgentData?: {
|
||||||
|
platform: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Platform = 'macos' | 'windows' | 'linux'
|
||||||
|
|
||||||
|
export function detectDesktopOS(): Platform | null {
|
||||||
|
const platform =
|
||||||
|
window.navigator.userAgentData?.platform.toLowerCase() ??
|
||||||
|
window.navigator.platform.toLowerCase()
|
||||||
|
|
||||||
|
if (platform.includes('mac')) return 'macos'
|
||||||
|
if (platform.includes('win')) return 'windows'
|
||||||
|
if (platform.includes('linux')) return 'linux'
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDesktopOS = () => {
|
||||||
|
const [desktopOS, setDesktopOS] = useState<Platform | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// note: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/platform#examples deprecated but use cases are still valid
|
||||||
|
// note: https://html.spec.whatwg.org/multipage/system-state.html#dom-navigator-platform-dev (e.g. "MacIntel", "Win32", "Linux x86_64", "Linux armv81")
|
||||||
|
setDesktopOS(detectDesktopOS())
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return desktopOS
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export function useLocalStorage<T>(
|
||||||
|
key: string,
|
||||||
|
initialValue: T,
|
||||||
|
): [T, (value: T | ((val: T) => T)) => void] {
|
||||||
|
const [storedValue, setStoredValue] = useState<T>(initialValue)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const item = window.localStorage.getItem(key)
|
||||||
|
setStoredValue(item ? JSON.parse(item) : initialValue)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
setStoredValue(initialValue)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleStorageChange = (event: StorageEvent) => {
|
||||||
|
if (event.key === key) {
|
||||||
|
setStoredValue(
|
||||||
|
event.newValue ? JSON.parse(event.newValue) : initialValue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('storage', handleStorageChange)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('storage', handleStorageChange)
|
||||||
|
}
|
||||||
|
}, [key, initialValue])
|
||||||
|
|
||||||
|
const setValue = (value: T | ((val: T) => T)) => {
|
||||||
|
try {
|
||||||
|
const valueToStore =
|
||||||
|
value instanceof Function ? value(storedValue) : value
|
||||||
|
|
||||||
|
setStoredValue(valueToStore)
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(valueToStore))
|
||||||
|
window.dispatchEvent(new Event('storage'))
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [storedValue, setValue]
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
export const useOutsideClick = (
|
||||||
|
ref: React.RefObject<HTMLDivElement>,
|
||||||
|
callback: () => void,
|
||||||
|
) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClick = (event: MouseEvent) => {
|
||||||
|
const path = event.composedPath()
|
||||||
|
if (ref.current && !path.includes(ref.current)) {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', handleClick, true)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', handleClick, true)
|
||||||
|
}
|
||||||
|
}, [ref, callback])
|
||||||
|
|
||||||
|
return ref
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { WebSocketProvider } from 'ethers'
|
||||||
|
|
||||||
|
import { config } from '~config'
|
||||||
|
|
||||||
|
import type { RequestArguments } from '~lib/request-arguments'
|
||||||
|
import type { WebSocketLike } from 'ethers'
|
||||||
|
|
||||||
|
type DesktopRequestArguments = RequestArguments & {
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
iconUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DesktopClient {
|
||||||
|
#rpcClient: WebSocketProvider | null = null
|
||||||
|
|
||||||
|
public stop() {
|
||||||
|
if (!this.#rpcClient) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('stop::')
|
||||||
|
this.#rpcClient?.destroy()
|
||||||
|
this.#rpcClient = null
|
||||||
|
|
||||||
|
// todo: publish disconnect message/event after https://github.com/status-im/status-desktop/issues/16014
|
||||||
|
}
|
||||||
|
|
||||||
|
public async send(args: DesktopRequestArguments) {
|
||||||
|
if (!this.#rpcClient) {
|
||||||
|
console.log('start::')
|
||||||
|
this.#rpcClient = new WebSocketProvider(
|
||||||
|
config.desktop.rpc.url,
|
||||||
|
'mainnet',
|
||||||
|
{
|
||||||
|
staticNetwork: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
;(this.#rpcClient.websocket as WebSocket).onclose = () => {
|
||||||
|
this.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitUntilOpen(this.#rpcClient.websocket)
|
||||||
|
|
||||||
|
console.log('client::', {
|
||||||
|
method: config.desktop.rpc.method,
|
||||||
|
params: [JSON.stringify(args)],
|
||||||
|
})
|
||||||
|
|
||||||
|
return await this.#rpcClient.send(config.desktop.rpc.method, [
|
||||||
|
JSON.stringify(args),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitUntilOpen(websocket: WebSocketLike) {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
if (websocket.readyState === WebSocket.OPEN) {
|
||||||
|
resolve()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (websocket.readyState === WebSocket.CLOSING) {
|
||||||
|
reject(new Error('The RPC server is closing'))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (websocket.readyState === WebSocket.CLOSED) {
|
||||||
|
reject(new Error('The RPC server is closed'))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
reject(new Error('Timed out to connect to the RPC server'))
|
||||||
|
}, 30 * 1000)
|
||||||
|
|
||||||
|
if (websocket.readyState === WebSocket.CONNECTING) {
|
||||||
|
console.info('Waiting for the RPC server to connect')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onopen = websocket.onopen?.bind(websocket)
|
||||||
|
websocket.onopen = event => {
|
||||||
|
onopen?.(event)
|
||||||
|
websocket.onopen = onopen!
|
||||||
|
clearTimeout(timeout)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onerror = websocket.onerror?.bind(websocket)
|
||||||
|
websocket.onerror = event => {
|
||||||
|
onerror?.(event)
|
||||||
|
websocket.onerror = onerror!
|
||||||
|
clearTimeout(timeout)
|
||||||
|
reject(new Error('Failed to connect to the RPC server'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
export function getFaviconUrl() {
|
||||||
|
const faviconElement =
|
||||||
|
document.querySelector<HTMLLinkElement>('link[rel="icon"]')
|
||||||
|
|
||||||
|
if (faviconElement) {
|
||||||
|
return faviconElement.href
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconElements =
|
||||||
|
document.querySelectorAll<HTMLLinkElement>('link[rel*="icon"]')
|
||||||
|
|
||||||
|
if (iconElements.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return iconElements[0].href
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
export class ProviderRpcError extends Error {
|
||||||
|
public code: number
|
||||||
|
public data: unknown
|
||||||
|
|
||||||
|
constructor(args: { code: number; message: string; data?: unknown }) {
|
||||||
|
super(args.message)
|
||||||
|
this.code = args.code
|
||||||
|
this.data = args.data
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://eips.ethereum.org/EIPS/eip-1193#request-1
|
||||||
|
*/
|
||||||
|
export const RequestArguments = z.object({
|
||||||
|
method: z.string(),
|
||||||
|
params: z
|
||||||
|
.union([z.array(z.unknown()).readonly(), z.record(z.unknown()).optional()])
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type RequestArguments = z.infer<typeof RequestArguments>
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import { RequestArguments } from '~lib/request-arguments'
|
||||||
|
|
||||||
|
export const ProviderMessage = z.discriminatedUnion('type', [
|
||||||
|
z.object({
|
||||||
|
type: z.literal('status:provider'),
|
||||||
|
data: RequestArguments,
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal('status:provider:disconnect'),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal('status:provider:data'),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
export type ProviderMessage = z.infer<typeof ProviderMessage>
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const ProxyMessage = z.discriminatedUnion('type', [
|
||||||
|
z.object({
|
||||||
|
type: z.literal('status:proxy:success'),
|
||||||
|
data: z.unknown(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal('status:proxy:error'),
|
||||||
|
error: z.object({
|
||||||
|
code: z.number(),
|
||||||
|
message: z.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
export type ProxyMessage = z.infer<typeof ProxyMessage>
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const ServiceWorkerMessage = z.discriminatedUnion('type', [
|
||||||
|
z.object({
|
||||||
|
type: z.literal('status:icon:clicked'),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
export type ServiceWorkerMessage = z.infer<typeof ServiceWorkerMessage>
|
|
@ -0,0 +1,15 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
font-family: theme(fontFamily.sans);
|
||||||
|
color: theme(colors.neutral.100);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: theme(fontFamily.sans) !important;
|
||||||
|
font-size: initial !important;
|
||||||
|
}
|
|
@ -0,0 +1,291 @@
|
||||||
|
import * as colors from '@status-im/colors'
|
||||||
|
import { scrollbarWidth } from 'tailwind-scrollbar-utilities'
|
||||||
|
import { fontFamily } from 'tailwindcss/defaultTheme'
|
||||||
|
import plugin from 'tailwindcss/plugin'
|
||||||
|
|
||||||
|
import type { Config } from 'tailwindcss'
|
||||||
|
|
||||||
|
export const screens = {
|
||||||
|
// We simulate the desktop first approach by using min-width 1px above the max-width of the previous breakpoint to match the design breakpoints
|
||||||
|
// Otherwise, we would have to use max-width approach and change the entire codebase styles
|
||||||
|
sm: '431px',
|
||||||
|
md: '641px',
|
||||||
|
'2md': '768px',
|
||||||
|
lg: '869px',
|
||||||
|
xl: '1024px',
|
||||||
|
'2xl': '1281px',
|
||||||
|
'3xl': '1441px',
|
||||||
|
// TODO to be defined by design for pro-users
|
||||||
|
'4xl': '1601px',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
darkMode: 'media',
|
||||||
|
future: {
|
||||||
|
hoverOnlyWhenSupported: true,
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
'./src/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
// './app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', ...fontFamily.sans],
|
||||||
|
mono: fontFamily.mono,
|
||||||
|
},
|
||||||
|
|
||||||
|
// use <Text /> from @status-im/components or arbitrary values
|
||||||
|
// note: https://tailwindcss.com/docs/font-size#providing-a-default-line-height for `[fontSize, { lineHeight?, letterSpacing?, fontWeight? }]`
|
||||||
|
fontSize: {
|
||||||
|
// note: not in design system
|
||||||
|
240: [
|
||||||
|
'15rem',
|
||||||
|
{
|
||||||
|
lineHeight: '13.25rem',
|
||||||
|
letterSpacing: '-0.6rem',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// note: not in design system
|
||||||
|
120: [
|
||||||
|
'7.5rem',
|
||||||
|
{
|
||||||
|
lineHeight: '5.25rem',
|
||||||
|
letterSpacing: '-0.1575rem',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
88: [
|
||||||
|
'5.5rem',
|
||||||
|
{
|
||||||
|
lineHeight: '5.25rem',
|
||||||
|
letterSpacing: '-0.1155rem',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// note: not in design system
|
||||||
|
76: [
|
||||||
|
'4.75rem',
|
||||||
|
{
|
||||||
|
lineHeight: '4.25rem',
|
||||||
|
letterSpacing: '-0.095rem',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
64: [
|
||||||
|
'4rem',
|
||||||
|
{
|
||||||
|
lineHeight: '4.25rem',
|
||||||
|
letterSpacing: '-0.08rem',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// note: not in design system
|
||||||
|
48: [
|
||||||
|
'3rem',
|
||||||
|
{
|
||||||
|
lineHeight: '3.125rem',
|
||||||
|
letterSpacing: '-0.06rem',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
40: [
|
||||||
|
'2.5rem',
|
||||||
|
{
|
||||||
|
lineHeight: '2.75rem',
|
||||||
|
letterSpacing: '-0.05rem',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
27: [
|
||||||
|
'1.6875rem',
|
||||||
|
{
|
||||||
|
lineHeight: '2rem',
|
||||||
|
letterSpacing: '-0.0354375rem',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
19: [
|
||||||
|
'1.1875rem',
|
||||||
|
{
|
||||||
|
lineHeight: '1.75rem',
|
||||||
|
letterSpacing: '-0.019rem',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
15: [
|
||||||
|
'0.9375rem',
|
||||||
|
{
|
||||||
|
lineHeight: '1.359375rem',
|
||||||
|
letterSpacing: '-0.0084375rem',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
13: [
|
||||||
|
'0.8125rem',
|
||||||
|
{
|
||||||
|
lineHeight: '1.1375rem',
|
||||||
|
letterSpacing: '-0.0024375rem',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
11: [
|
||||||
|
'0.6875rem',
|
||||||
|
{
|
||||||
|
lineHeight: '0.97625rem',
|
||||||
|
letterSpacing: '-0.0034375rem',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
colors: colors,
|
||||||
|
|
||||||
|
fontWeight: {
|
||||||
|
regular: '400',
|
||||||
|
400: '400',
|
||||||
|
medium: '500',
|
||||||
|
500: '500',
|
||||||
|
semibold: '600',
|
||||||
|
600: '600',
|
||||||
|
bold: '700',
|
||||||
|
700: '700',
|
||||||
|
},
|
||||||
|
|
||||||
|
boxShadow: {
|
||||||
|
1: '0px 2px 20px rgba(9, 16, 28, 0.04)',
|
||||||
|
2: '0px 4px 20px rgba(9, 16, 28, 0.08)',
|
||||||
|
3: '0px 8px 30px rgba(9, 16, 28, 0.12);',
|
||||||
|
},
|
||||||
|
|
||||||
|
extend: {
|
||||||
|
spacing: {
|
||||||
|
30: '7.5rem',
|
||||||
|
},
|
||||||
|
|
||||||
|
borderRadius: {
|
||||||
|
'4xl': '2rem',
|
||||||
|
},
|
||||||
|
|
||||||
|
maxWidth: {
|
||||||
|
page: '1504',
|
||||||
|
},
|
||||||
|
|
||||||
|
transitionProperty: {
|
||||||
|
height: 'height',
|
||||||
|
},
|
||||||
|
|
||||||
|
screens,
|
||||||
|
|
||||||
|
keyframes: {
|
||||||
|
heightIn: {
|
||||||
|
from: { height: '0' },
|
||||||
|
// to: { height: 296 },
|
||||||
|
to: { height: 'var(--radix-navigation-menu-viewport-height)' },
|
||||||
|
},
|
||||||
|
heightOut: {
|
||||||
|
from: { height: 'var(--radix-navigation-menu-viewport-height)' },
|
||||||
|
// from: { height: 296 },
|
||||||
|
to: { height: '0' },
|
||||||
|
},
|
||||||
|
slide: {
|
||||||
|
from: {
|
||||||
|
transform: 'translateX(0)',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
transform: 'translateX(-100%)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
slideInFromBottom: {
|
||||||
|
from: {
|
||||||
|
transform: 'translateY(200px)',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
transform: 'translateY(0)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
marquee1: {
|
||||||
|
'0%': { transform: 'translateX(0%)' },
|
||||||
|
'100%': { transform: 'translateX(-100%)' },
|
||||||
|
},
|
||||||
|
explanationIn: {
|
||||||
|
'0%': { opacity: '0', transform: 'scale(0.7)' },
|
||||||
|
'80%': { opacity: '0.8', transform: 'scale(1.02)' },
|
||||||
|
'100%': { opacity: '1', transform: 'scale(1)' },
|
||||||
|
},
|
||||||
|
explanationOut: {
|
||||||
|
'0%': { opacity: '1', transform: 'scale(1)' },
|
||||||
|
'100%': { opacity: '0', transform: 'scale(0.7)' },
|
||||||
|
},
|
||||||
|
explanationSlide: {
|
||||||
|
'0%': {
|
||||||
|
transform: 'translateY(100px)',
|
||||||
|
opacity: '0',
|
||||||
|
},
|
||||||
|
'100%': {
|
||||||
|
transform: 'translateY(0px)',
|
||||||
|
opacity: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
marquee2: {
|
||||||
|
'0%': { transform: 'translateX(100%)' },
|
||||||
|
'100%': { transform: 'translateX(0%)' },
|
||||||
|
},
|
||||||
|
|
||||||
|
// dialog
|
||||||
|
'overlay-enter': {
|
||||||
|
from: { opacity: '0' },
|
||||||
|
to: { opacity: '1' },
|
||||||
|
},
|
||||||
|
'overlay-exit': {
|
||||||
|
'0%': { opacity: '1' },
|
||||||
|
'100%': { opacity: '0' },
|
||||||
|
},
|
||||||
|
'drawer-enter': {
|
||||||
|
from: { opacity: '0', transform: 'translateY(100%)' },
|
||||||
|
to: { opacity: '1', transform: 'translateY(0)' },
|
||||||
|
},
|
||||||
|
'drawer-exit': {
|
||||||
|
from: { opacity: '1', transform: 'translateY(0)' },
|
||||||
|
to: { opacity: '0', transform: 'translateY(100%)' },
|
||||||
|
},
|
||||||
|
'dialog-enter': {
|
||||||
|
from: {
|
||||||
|
opacity: '0',
|
||||||
|
transform: 'translate(-50%, -48%) scale(0.96)',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: '1',
|
||||||
|
transform: 'translate(-50%, -50%) scale(1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'dialog-exit': {
|
||||||
|
from: { opacity: '1', transform: 'translate(-50%, -50%) scale(1)' },
|
||||||
|
to: { opacity: '0', transform: 'translate(-50%, -48%) scale(0.96)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
animation: {
|
||||||
|
heightIn: 'heightIn 250ms ease',
|
||||||
|
heightOut: 'heightOut 250ms ease',
|
||||||
|
slide: '45s slide infinite linear',
|
||||||
|
slideInFromBottom: 'slideInFromBottom 0.5s ease',
|
||||||
|
marquee1: 'marquee1 180s linear infinite',
|
||||||
|
marquee2: 'marquee2 180s linear infinite',
|
||||||
|
explanationIn: 'explanationIn 150ms ease',
|
||||||
|
explanationOut: 'explanationIn 150ms ease reverse',
|
||||||
|
explanationSlideUp: 'explanationSlide 180ms ease',
|
||||||
|
explanationSlideDown: 'explanationSlide 180ms ease reverse',
|
||||||
|
},
|
||||||
|
|
||||||
|
backgroundImage: {
|
||||||
|
'gradient-hatching':
|
||||||
|
'repeating-linear-gradient(-75deg, #E7EAEE, #E7EAEE 4px, rgba(161, 171, 188, 0.2) 0px, rgba(161, 171, 188, 0.2) 6px)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
require('tailwindcss-animate'),
|
||||||
|
// add scrollbar utilities before lands in tailwindcss
|
||||||
|
// @see https://github.com/tailwindlabs/tailwindcss/pull/5732
|
||||||
|
scrollbarWidth(),
|
||||||
|
// scrollbarColor(),
|
||||||
|
// scrollbarGutter(),
|
||||||
|
|
||||||
|
plugin(({ matchUtilities }) => {
|
||||||
|
matchUtilities({
|
||||||
|
perspective: value => ({
|
||||||
|
perspective: value,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
} satisfies Config
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"extends": "plasmo/templates/tsconfig.base",
|
||||||
|
"exclude": ["node_modules"],
|
||||||
|
"include": [".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx", "global.d.ts"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"~*": ["./src/*"],
|
||||||
|
},
|
||||||
|
"baseUrl": ".",
|
||||||
|
"lib": ["WebWorker", "dom", "WebWorker.ImportScripts"],
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "ESNext",
|
||||||
|
},
|
||||||
|
}
|