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/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
|
||||
|
||||
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",
|
||||
},
|
||||
}
|