Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a3993d42b | ||
|
|
bab39980e7 | ||
|
|
48295c0604 | ||
|
|
3fd39cea0d | ||
|
|
1fe867e205 | ||
|
|
f5991387ef | ||
|
|
3f3c63c01e | ||
|
|
f80cf0e17e | ||
|
|
10b7648857 |
@@ -1,5 +1,12 @@
|
||||
# @thirdweb-dev/auth
|
||||
|
||||
## 3.2.42
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`3f3c63c0`](https://github.com/thirdweb-dev/js/commit/3f3c63c01e34242ae1f074e62b51787b305c059e)]:
|
||||
- @thirdweb-dev/wallets@1.2.1
|
||||
|
||||
## 3.2.41
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/auth",
|
||||
"version": "3.2.41",
|
||||
"version": "3.2.42",
|
||||
"main": "dist/thirdweb-dev-auth.cjs.js",
|
||||
"module": "dist/thirdweb-dev-auth.esm.js",
|
||||
"browser": {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @thirdweb-dev/chains
|
||||
|
||||
## 0.1.52
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1534](https://github.com/thirdweb-dev/js/pull/1534) [`48295c06`](https://github.com/thirdweb-dev/js/commit/48295c060499371035980d08e362d9858d0fc18b) Thanks [@ElasticBottle](https://github.com/ElasticBottle)! - feat(chains): add new chain information
|
||||
|
||||
## 0.1.51
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -14,8 +14,8 @@ export default {
|
||||
],
|
||||
"faucets": [],
|
||||
"nativeCurrency": {
|
||||
"name": "MAP",
|
||||
"symbol": "MAP",
|
||||
"name": "MAPO",
|
||||
"symbol": "MAPO",
|
||||
"decimals": 18
|
||||
},
|
||||
"infoURL": "https://maplabs.io",
|
||||
|
||||
@@ -21,7 +21,7 @@ export default {
|
||||
}
|
||||
],
|
||||
"infoURL": "https://manta-testnet.caldera.dev/",
|
||||
"shortName": "manta",
|
||||
"shortName": "mantaTestnet",
|
||||
"chainId": 3441005,
|
||||
"networkId": 3441005,
|
||||
"icon": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/chains",
|
||||
"version": "0.1.51",
|
||||
"version": "0.1.52",
|
||||
"main": "dist/thirdweb-dev-chains.cjs.js",
|
||||
"module": "dist/thirdweb-dev-chains.esm.js",
|
||||
"browser": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,15 @@
|
||||
# thirdweb
|
||||
|
||||
## 0.12.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`3fd39cea`](https://github.com/thirdweb-dev/js/commit/3fd39cea0df71f80255106329db62660f2fd6e3a), [`3f3c63c0`](https://github.com/thirdweb-dev/js/commit/3f3c63c01e34242ae1f074e62b51787b305c059e), [`48295c06`](https://github.com/thirdweb-dev/js/commit/48295c060499371035980d08e362d9858d0fc18b), [`48295c06`](https://github.com/thirdweb-dev/js/commit/48295c060499371035980d08e362d9858d0fc18b)]:
|
||||
- @thirdweb-dev/sdk@3.10.61
|
||||
- @thirdweb-dev/wallets@1.2.1
|
||||
- @thirdweb-dev/chains@0.1.52
|
||||
- @thirdweb-dev/auth@3.2.42
|
||||
|
||||
## 0.12.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "thirdweb",
|
||||
"main": "dist/cli/index.js",
|
||||
"version": "0.12.3",
|
||||
"version": "0.12.4",
|
||||
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/cli",
|
||||
"author": "thirdweb eng <[email protected]>",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @thirdweb-dev/react-core
|
||||
|
||||
## 3.15.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1550](https://github.com/thirdweb-dev/js/pull/1550) [`3f3c63c0`](https://github.com/thirdweb-dev/js/commit/3f3c63c01e34242ae1f074e62b51787b305c059e) Thanks [@MananTank](https://github.com/MananTank)! - ConnectWallet v3 updates
|
||||
|
||||
- Updated dependencies [[`3fd39cea`](https://github.com/thirdweb-dev/js/commit/3fd39cea0df71f80255106329db62660f2fd6e3a), [`3f3c63c0`](https://github.com/thirdweb-dev/js/commit/3f3c63c01e34242ae1f074e62b51787b305c059e), [`48295c06`](https://github.com/thirdweb-dev/js/commit/48295c060499371035980d08e362d9858d0fc18b), [`48295c06`](https://github.com/thirdweb-dev/js/commit/48295c060499371035980d08e362d9858d0fc18b)]:
|
||||
- @thirdweb-dev/sdk@3.10.61
|
||||
- @thirdweb-dev/wallets@1.2.1
|
||||
- @thirdweb-dev/chains@0.1.52
|
||||
- @thirdweb-dev/auth@3.2.42
|
||||
|
||||
## 3.14.41
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/react-core",
|
||||
"version": "3.14.41",
|
||||
"version": "3.15.0",
|
||||
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react-core",
|
||||
"author": "thirdweb eng <[email protected]>",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -17,6 +17,7 @@ export type WalletClass<I extends WalletInstance = WalletInstance> = {
|
||||
};
|
||||
|
||||
export type WalletConfig<I extends WalletInstance = WalletInstance> = {
|
||||
category?: "socialLogin" | "walletLogin";
|
||||
id: string;
|
||||
meta: (typeof AbstractClientWallet)["meta"];
|
||||
create: (options: WalletOptions) => I;
|
||||
@@ -39,6 +40,11 @@ export type WalletConfig<I extends WalletInstance = WalletInstance> = {
|
||||
* * ConnectWallet modal will reopen once the personal wallet is connected so that you can render UI for connecting your wallet as the next step
|
||||
*/
|
||||
personalWallets?: WalletConfig[];
|
||||
|
||||
/**
|
||||
* If true, this wallet will be tagged as "recommended" in ConnectWallet Modal and will be shown at the top of the list
|
||||
*/
|
||||
recommended?: boolean;
|
||||
};
|
||||
|
||||
export type ConnectUIProps<I extends WalletInstance = WalletInstance> = {
|
||||
@@ -127,6 +133,13 @@ export type ConnectUIProps<I extends WalletInstance = WalletInstance> = {
|
||||
* List of all supported wallets including your wallet.
|
||||
*/
|
||||
supportedWallets: WalletConfig[];
|
||||
|
||||
/**
|
||||
* Size of the modal
|
||||
*
|
||||
* This is always `compact` on React Native
|
||||
*/
|
||||
modalSize: "compact" | "wide";
|
||||
};
|
||||
|
||||
export type SelectUIProps<I extends WalletInstance = WalletInstance> = {
|
||||
@@ -156,4 +169,11 @@ export type SelectUIProps<I extends WalletInstance = WalletInstance> = {
|
||||
* theme of the connect wallet modal
|
||||
*/
|
||||
theme: "dark" | "light";
|
||||
|
||||
/**
|
||||
* Size of the modal
|
||||
*
|
||||
* This is always `compact` on React Native
|
||||
*/
|
||||
modalSize: "compact" | "wide";
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# @thirdweb-dev/react-native-compat
|
||||
|
||||
## 0.2.61
|
||||
|
||||
## 0.2.60
|
||||
|
||||
## 0.2.59
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/react-native-compat",
|
||||
"description": "Shims for Thirdweb in React Native Projects",
|
||||
"version": "0.2.60",
|
||||
"version": "0.2.61",
|
||||
"author": "thirdweb eng <[email protected]>",
|
||||
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react-native-compat",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,5 +1,73 @@
|
||||
# @thirdweb-dev/react-native
|
||||
|
||||
## 0.2.61
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1592](https://github.com/thirdweb-dev/js/pull/1592) [`1fe867e2`](https://github.com/thirdweb-dev/js/commit/1fe867e20510a43d2a19ba553709aeaca1d46bb3) Thanks [@iketw](https://github.com/iketw)! - Improved ConnectWallet UX/DevX :)
|
||||
|
||||
1. Devs can now pass a `recommended` field to `supportedWallets` to recommend wallets to users.
|
||||
|
||||
When recommending a wallet, this wallet will show up at the top of the wallets list.
|
||||
|
||||
```javascript
|
||||
import { metamaskWallet, ThirdwebProvider } from "@thirdweb-dev/react-native";
|
||||
|
||||
<ThirdwebProvider
|
||||
clientId="your-client-id"
|
||||
supportedWallets={[
|
||||
metamaskWallet({
|
||||
recommended: true,
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<App />
|
||||
</ThirdwebProvider>;
|
||||
```
|
||||
|
||||
2. Better theme customization
|
||||
|
||||
You can now more easily customize the theme of the ConnectWallet component:
|
||||
|
||||
```javascript
|
||||
import { ConnectWallet, darkTheme } from "@thirdweb-dev/react-native";
|
||||
|
||||
<ConnectWallet
|
||||
theme={darkTheme({
|
||||
colors: {
|
||||
textPrimary: "yellow",
|
||||
textSecondary: "blue",
|
||||
},
|
||||
})}
|
||||
buttonTitle="Enter web3"
|
||||
modalTitle="Sign in"
|
||||
/>;
|
||||
```
|
||||
|
||||
Note that you can still pass `light` or `dark` if you want to use one of the predefined thems :)
|
||||
|
||||
3. You can now pass your Privacy Policy and Terms of Service urls, they will show up
|
||||
at the bottom of the modal:
|
||||
|
||||
```javascript
|
||||
import { ConnectWallet, darkTheme } from "@thirdweb-dev/react-native";
|
||||
|
||||
<ConnectWallet
|
||||
privacyPolicyUrl="https://your-privacy-policy"
|
||||
termsOfServiceUrl="https://your-terms-of-service"
|
||||
/>;
|
||||
```
|
||||
|
||||
4. Fixed a bug when you only specify an email wallet. In this case the app shouldn't auto connect, it should show the email field.
|
||||
|
||||
- [#1550](https://github.com/thirdweb-dev/js/pull/1550) [`3f3c63c0`](https://github.com/thirdweb-dev/js/commit/3f3c63c01e34242ae1f074e62b51787b305c059e) Thanks [@MananTank](https://github.com/MananTank)! - ConnectWallet v3 updates
|
||||
|
||||
- Updated dependencies [[`3fd39cea`](https://github.com/thirdweb-dev/js/commit/3fd39cea0df71f80255106329db62660f2fd6e3a), [`3f3c63c0`](https://github.com/thirdweb-dev/js/commit/3f3c63c01e34242ae1f074e62b51787b305c059e), [`48295c06`](https://github.com/thirdweb-dev/js/commit/48295c060499371035980d08e362d9858d0fc18b), [`48295c06`](https://github.com/thirdweb-dev/js/commit/48295c060499371035980d08e362d9858d0fc18b)]:
|
||||
- @thirdweb-dev/[email protected]
|
||||
- @thirdweb-dev/[email protected]
|
||||
- @thirdweb-dev/[email protected]
|
||||
- @thirdweb-dev/[email protected]
|
||||
|
||||
## 0.2.60
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/react-native",
|
||||
"version": "0.2.60",
|
||||
"version": "0.2.61",
|
||||
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react-native",
|
||||
"author": "thirdweb eng <[email protected]>",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { IconStyleProp } from "./types";
|
||||
import Svg, { Path, Rect } from "react-native-svg";
|
||||
|
||||
const Email = ({ width, height }: IconStyleProp) => {
|
||||
return (
|
||||
<Svg width={width} height={height} viewBox="0 0 18 18" fill="none">
|
||||
<Rect
|
||||
x="0.633333"
|
||||
y="0.633333"
|
||||
width="16.4667"
|
||||
height="16.4667"
|
||||
rx="4.43333"
|
||||
fill="#3385FF"
|
||||
/>
|
||||
<Rect x="2.2168" y="4.43359" width="13.3" height="8.86667" fill="white" />
|
||||
<Path
|
||||
d="M14.7775 2.95557H2.95532C2.14254 2.95557 1.48493 3.62057 1.48493 4.43334L1.47754 13.3C1.47754 14.1128 2.14254 14.7778 2.95532 14.7778H14.7775C15.5903 14.7778 16.2553 14.1128 16.2553 13.3V4.43334C16.2553 3.62057 15.5903 2.95557 14.7775 2.95557ZM14.7775 5.91112L8.86643 9.60557L2.95532 5.91112V4.43334L8.86643 8.12779L14.7775 4.43334V5.91112Z"
|
||||
fill="#3385FF"
|
||||
/>
|
||||
<Rect
|
||||
x="0.633333"
|
||||
y="0.633333"
|
||||
width="16.4667"
|
||||
height="16.4667"
|
||||
rx="4.43333"
|
||||
stroke="#1177FD"
|
||||
stroke-width="1.26667"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Email;
|
||||
@@ -1,7 +1,11 @@
|
||||
import { IconStyleProp } from "./types";
|
||||
import Svg, { Path } from "react-native-svg";
|
||||
|
||||
const RightArrowIcon = ({ width, height, color }: IconStyleProp) => {
|
||||
const RightArrowIcon = ({
|
||||
width,
|
||||
height,
|
||||
color = "#646D7A",
|
||||
}: IconStyleProp) => {
|
||||
return (
|
||||
<Svg width={width} height={height} viewBox="0 0 5 8" fill="none">
|
||||
<Path
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { IconStyleProp } from "./types";
|
||||
import Svg, { ClipPath, Defs, G, Path, Rect } from "react-native-svg";
|
||||
|
||||
const ThirdwebLogo = ({ width, height, color }: IconStyleProp) => {
|
||||
return (
|
||||
<Svg width={width} height={height} viewBox="0 0 26 16" fill="none">
|
||||
<G clip-path="url(#clip0_3185_13519)">
|
||||
<Path
|
||||
d="M13.6795 0.5C13.8386 0.531779 13.9887 0.595721 14.1196 0.6875C14.2673 0.797724 14.3804 0.944328 14.4467 1.11141C14.8623 2.10272 15.279 3.09348 15.6968 4.0837L17.089 7.39022C17.3322 7.96902 17.5736 8.5462 17.8202 9.12337C17.8727 9.24121 17.8998 9.36791 17.8998 9.49592C17.8998 9.62393 17.8727 9.75064 17.8202 9.86848C17.1147 11.5446 16.4086 13.2217 15.7019 14.9C15.6444 15.0572 15.542 15.196 15.4062 15.3009C15.2703 15.4057 15.1065 15.4724 14.933 15.4935C14.7188 15.5315 14.4972 15.4982 14.3057 15.399C14.1143 15.2998 13.9645 15.1409 13.8816 14.9489C13.4843 14.0196 13.0956 13.087 12.7034 12.1543C12.0367 10.575 11.3717 8.99457 10.7085 7.41304C10.1159 6.00543 9.52401 4.59891 8.93265 3.19348C8.73572 2.72717 8.54564 2.25761 8.34015 1.79293C8.28363 1.66705 8.25686 1.53087 8.26173 1.39405C8.26661 1.25722 8.30301 1.12309 8.36836 1.00114C8.43371 0.879187 8.52641 0.772402 8.6399 0.688339C8.75339 0.604275 8.8849 0.544988 9.02512 0.514673C9.04225 0.514673 9.05766 0.504891 9.07478 0.5H13.6795Z"
|
||||
fill={color}
|
||||
/>
|
||||
<Path
|
||||
d="M5.41171 0.5C5.46479 0.514674 5.51617 0.526087 5.56754 0.542392C5.70064 0.581915 5.82278 0.64928 5.925 0.739555C6.02722 0.829829 6.10694 0.940727 6.15833 1.06413C6.61498 2.13913 7.06821 3.21413 7.51801 4.28913C7.96781 5.36413 8.42446 6.45109 8.88796 7.55L9.56095 9.14456C9.61135 9.25806 9.63731 9.38004 9.63731 9.50326C9.63731 9.62648 9.61135 9.74846 9.56095 9.86196C9.31094 10.4587 9.05978 11.0565 8.80748 11.6554L7.42554 14.931C7.35297 15.1012 7.22895 15.2471 7.0692 15.3501C6.90946 15.4532 6.72117 15.5087 6.52822 15.5098C6.32467 15.5125 6.12519 15.4554 5.95696 15.3463C5.78872 15.2372 5.65991 15.0814 5.58809 14.9C5.15998 13.8712 4.73187 12.844 4.29348 11.8168L2.53823 7.64946L0.753866 3.41033C0.529536 2.87554 0.305206 2.33913 0.0688893 1.80435C-0.0308499 1.58161 -0.0356895 1.33067 0.0553906 1.10459C0.146471 0.878515 0.326331 0.695028 0.556936 0.592935C0.641014 0.560457 0.727427 0.533759 0.815514 0.513044L5.41171 0.5Z"
|
||||
fill={color}
|
||||
/>
|
||||
<Path
|
||||
d="M25.9999 9.64348C25.8766 10.0478 25.6968 10.4293 25.5255 10.8239C25.0609 11.9315 24.5934 13.0397 24.123 14.1484C24.0151 14.4109 23.9055 14.6554 23.8011 14.9114C23.7264 15.091 23.595 15.244 23.4251 15.3495C23.2552 15.4549 23.0551 15.5075 22.8524 15.5C22.6589 15.495 22.4714 15.4353 22.3137 15.3284C22.156 15.2216 22.0354 15.0724 21.9671 14.9C21.2889 13.2848 20.6097 11.6696 19.9292 10.0543C19.3425 8.66522 18.7534 7.27283 18.162 5.87717C17.5912 4.51087 17.0204 3.14456 16.4496 1.77826C16.3942 1.65338 16.3684 1.51844 16.374 1.383C16.3795 1.24756 16.4163 1.11494 16.4816 0.994528C16.547 0.874117 16.6393 0.768861 16.7522 0.686219C16.865 0.603577 16.9955 0.545569 17.1345 0.516304L17.1911 0.5H21.7924L21.8352 0.513044C21.9973 0.545507 22.1481 0.616718 22.2733 0.719974C22.3985 0.82323 22.4941 0.955132 22.551 1.10326C22.8181 1.73913 23.0864 2.37554 23.3558 3.0125C24.0579 4.68206 24.76 6.35163 25.4621 8.02119C25.6471 8.46141 25.8526 8.89348 26.0033 9.34511L25.9999 9.64348Z"
|
||||
fill={color}
|
||||
/>
|
||||
</G>
|
||||
<Defs>
|
||||
<ClipPath id="clip0_3185_13519">
|
||||
<Rect
|
||||
width={width}
|
||||
height={height}
|
||||
fill={color}
|
||||
transform="translate(0 0.5)"
|
||||
/>
|
||||
</ClipPath>
|
||||
</Defs>
|
||||
</Svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThirdwebLogo;
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ThemeProvider } from "../styles/ThemeProvider";
|
||||
import {
|
||||
ConnectWalletDetailsProps,
|
||||
WalletDetailsButton,
|
||||
@@ -15,6 +14,8 @@ import { ConnectWalletButton } from "./ConnectWalletFlow/ConnectWalletButton";
|
||||
import { ConnectWalletButtonProps } from "./ConnectWalletFlow/ConnectWalletButton";
|
||||
import BaseButton from "./base/BaseButton";
|
||||
import Text from "./base/Text";
|
||||
import { useUIContext } from "../providers/ui-context-provider";
|
||||
import { ThemeProvider } from "../styles/ThemeProvider";
|
||||
|
||||
export type ConnectWalletProps = {
|
||||
/**
|
||||
@@ -54,6 +55,8 @@ export const ConnectWallet = ({
|
||||
extraRows,
|
||||
hideTestnetFaucet,
|
||||
switchToActiveChain,
|
||||
termsOfServiceUrl,
|
||||
privacyPolicyUrl,
|
||||
}: ConnectWalletProps) => {
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
const address = useAddress();
|
||||
@@ -62,6 +65,7 @@ export const ConnectWallet = ({
|
||||
const { activeChain } = useWalletContext();
|
||||
const switchChain = useSwitchChain();
|
||||
const [switching, setSwitching] = useState(false);
|
||||
const setTheme = useUIContext().setTheme;
|
||||
|
||||
useEffect(() => {
|
||||
Animated.timing(fadeAnim, {
|
||||
@@ -71,6 +75,12 @@ export const ConnectWallet = ({
|
||||
}).start();
|
||||
}, [fadeAnim]);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme) {
|
||||
setTheme(theme);
|
||||
}
|
||||
}, [setTheme, theme]);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Animated.View style={{ opacity: fadeAnim }}>
|
||||
@@ -112,6 +122,8 @@ export const ConnectWallet = ({
|
||||
modalTitle={modalTitle}
|
||||
buttonTitle={buttonTitle}
|
||||
theme={theme}
|
||||
termsOfServiceUrl={termsOfServiceUrl}
|
||||
privacyPolicyUrl={privacyPolicyUrl}
|
||||
/>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
@@ -119,11 +119,15 @@ const ConnectAppField = () => {
|
||||
borderRadius="md"
|
||||
>
|
||||
<TextInput
|
||||
onChangeText={onAddressChangeText}
|
||||
flex={1}
|
||||
value={wcUri}
|
||||
placeholder={"wc://..."}
|
||||
placeholderTextColor={theme.colors.textSecondary}
|
||||
textInputProps={{
|
||||
onChangeText: onAddressChangeText,
|
||||
value: wcUri,
|
||||
placeholder: "wc://...",
|
||||
placeholderTextColor: theme.colors.textSecondary,
|
||||
}}
|
||||
containerProps={{
|
||||
flex: 1,
|
||||
}}
|
||||
/>
|
||||
<BaseButton
|
||||
onPress={() => onWCPress()}
|
||||
|
||||
+39
-49
@@ -18,11 +18,11 @@ import { useSmartWallet } from "../../providers/context-provider";
|
||||
import BaseButton from "../base/BaseButton";
|
||||
import { SmartWalletAdditionalActions } from "./SmartWalletAdditionalActions";
|
||||
import Text from "../base/Text";
|
||||
import { useAppTheme } from "../../styles/hooks";
|
||||
import { LocalWalletImportModal } from "../ConnectWalletFlow/LocalWalletImportModal";
|
||||
import { IconTextButton } from "../base/IconTextButton";
|
||||
import MoneyIcon from "../../assets/money";
|
||||
import { TWModal } from "../base/modal/TWModal";
|
||||
import { ThemeProvider } from "../../styles/ThemeProvider";
|
||||
|
||||
const MODAL_HEIGHT = Dimensions.get("window").height * 0.7;
|
||||
const DEVICE_WIDTH = Dimensions.get("window").width;
|
||||
@@ -40,7 +40,6 @@ export const ConnectWalletDetailsModal = ({
|
||||
address?: string;
|
||||
hideTestnetFaucet?: boolean;
|
||||
}) => {
|
||||
const theme = useAppTheme();
|
||||
const [isExportModalVisible, setIsExportModalVisible] = useState(false);
|
||||
const activeWallet = useWallet();
|
||||
const chain = useChain();
|
||||
@@ -126,11 +125,7 @@ export const ConnectWalletDetailsModal = ({
|
||||
<Text variant="bodySmall">Backup wallet</Text>
|
||||
</View>
|
||||
</>
|
||||
<RightArrowIcon
|
||||
height={10}
|
||||
width={10}
|
||||
color={theme.colors.iconPrimary}
|
||||
/>
|
||||
<RightArrowIcon height={10} width={10} />
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
@@ -147,11 +142,7 @@ export const ConnectWalletDetailsModal = ({
|
||||
<Text variant="bodySmall">Import wallet</Text>
|
||||
</View>
|
||||
</>
|
||||
<RightArrowIcon
|
||||
height={10}
|
||||
width={10}
|
||||
color={theme.colors.iconPrimary}
|
||||
/>
|
||||
<RightArrowIcon height={10} width={10} />
|
||||
</BaseButton>
|
||||
|
||||
<LocalWalletImportModal
|
||||
@@ -178,48 +169,47 @@ export const ConnectWalletDetailsModal = ({
|
||||
onExportLocalWalletPress,
|
||||
onWalletImported,
|
||||
smartWallet,
|
||||
theme.colors.iconPrimary,
|
||||
]);
|
||||
|
||||
return (
|
||||
<TWModal isVisible={isVisible} onBackdropPress={onClosePress}>
|
||||
<View
|
||||
style={[styles.modal, { backgroundColor: theme.colors.background }]}
|
||||
>
|
||||
<Box style={styles.contentContainer}>
|
||||
<ExportLocalWalletModal
|
||||
isVisible={isExportModalVisible}
|
||||
onClose={onExportModalClose}
|
||||
/>
|
||||
<WalletDetailsModalHeader
|
||||
address={address}
|
||||
onDisconnectPress={onDisconnectPress}
|
||||
onAddressCopied={onAddressCopied}
|
||||
/>
|
||||
<View style={styles.currentNetwork}>
|
||||
<Text variant="bodySmallSecondary">Current Network</Text>
|
||||
</View>
|
||||
<NetworkButton chain={chain} enableSwitchModal={true} />
|
||||
{!hideTestnetFaucet && chain?.testnet && chain?.faucets?.length ? (
|
||||
<IconTextButton
|
||||
mt="xs"
|
||||
text="Request Testnet Funds"
|
||||
icon={<MoneyIcon height={10} width={10} />}
|
||||
onPress={() => {
|
||||
if (chain?.faucets?.[0]) {
|
||||
Linking.openURL(chain.faucets[0]);
|
||||
}
|
||||
}}
|
||||
<ThemeProvider>
|
||||
<TWModal isVisible={isVisible} onBackdropPress={onClosePress}>
|
||||
<Box backgroundColor="background" style={styles.modal}>
|
||||
<Box style={styles.contentContainer}>
|
||||
<ExportLocalWalletModal
|
||||
isVisible={isExportModalVisible}
|
||||
onClose={onExportModalClose}
|
||||
/>
|
||||
) : null}
|
||||
{getAdditionalActions()}
|
||||
{extraRows ? extraRows({}) : null}
|
||||
{addressCopied === true ? (
|
||||
<Toast text={"Address copied to clipboard"} />
|
||||
) : null}
|
||||
<WalletDetailsModalHeader
|
||||
address={address}
|
||||
onDisconnectPress={onDisconnectPress}
|
||||
onAddressCopied={onAddressCopied}
|
||||
/>
|
||||
<View style={styles.currentNetwork}>
|
||||
<Text variant="bodySmallSecondary">Current Network</Text>
|
||||
</View>
|
||||
<NetworkButton chain={chain} enableSwitchModal={true} />
|
||||
{!hideTestnetFaucet && chain?.testnet && chain?.faucets?.length ? (
|
||||
<IconTextButton
|
||||
mt="xs"
|
||||
text="Request Testnet Funds"
|
||||
icon={<MoneyIcon height={10} width={10} />}
|
||||
onPress={() => {
|
||||
if (chain?.faucets?.[0]) {
|
||||
Linking.openURL(chain.faucets[0]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{getAdditionalActions()}
|
||||
{extraRows ? extraRows({}) : null}
|
||||
{addressCopied === true ? (
|
||||
<Toast text={"Address copied to clipboard"} />
|
||||
) : null}
|
||||
</Box>
|
||||
</Box>
|
||||
</View>
|
||||
</TWModal>
|
||||
</TWModal>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+245
-23
@@ -1,11 +1,16 @@
|
||||
import { walletIds } from "@thirdweb-dev/wallets";
|
||||
import { localWallet } from "../../../wallets/wallets/local-wallet";
|
||||
import { ModalFooter } from "../../base/modal/ModalFooter";
|
||||
import { ModalHeaderTextClose } from "../../base/modal/ModalHeaderTextClose";
|
||||
import { ChooseWalletContent } from "./ChooseWalletContent";
|
||||
import { WalletConfig } from "@thirdweb-dev/react-core";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Box from "../../base/Box";
|
||||
import Text from "../../base/Text";
|
||||
import ThirdwebLogo from "../../../assets/thirdweb-logo";
|
||||
import { useAppTheme } from "../../../styles/hooks";
|
||||
import { ChooseWalletContent } from "./ChooseWalletContent";
|
||||
import { BaseButton, ImageSvgUri } from "../../base";
|
||||
import { ActivityIndicator, Linking } from "react-native";
|
||||
import { useTheme } from "@shopify/restyle";
|
||||
|
||||
export type ChooseWalletProps = {
|
||||
headerText?: ReactNode | string;
|
||||
@@ -14,7 +19,8 @@ export type ChooseWalletProps = {
|
||||
onClose: () => void;
|
||||
wallets: WalletConfig<any>[];
|
||||
excludeWalletIds?: string[];
|
||||
showGuestWalletAsButton?: boolean;
|
||||
termsOfServiceUrl?: string;
|
||||
privacyPolicyUrl?: string;
|
||||
};
|
||||
|
||||
export function ChooseWallet({
|
||||
@@ -24,40 +30,256 @@ export function ChooseWallet({
|
||||
onChooseWallet,
|
||||
onClose,
|
||||
excludeWalletIds = [],
|
||||
showGuestWalletAsButton = false,
|
||||
termsOfServiceUrl,
|
||||
privacyPolicyUrl,
|
||||
}: ChooseWalletProps) {
|
||||
const theme = useAppTheme();
|
||||
const themeLightDark = useTheme();
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [isConnectAWalletEnabled, setIsConnectAWalletEnabled] = useState(false);
|
||||
|
||||
const guestWallet = wallets.find((w) => w.id === walletIds.localWallet);
|
||||
const emailWallet = wallets.find((w) => w.id === walletIds.magicLink);
|
||||
const connectionWallets = wallets
|
||||
.filter(
|
||||
(wallet) =>
|
||||
wallet.id !== walletIds.magicLink &&
|
||||
wallet.id !== walletIds.localWallet,
|
||||
)
|
||||
.slice(0, 2);
|
||||
|
||||
const showToSPrivacyPolicy = termsOfServiceUrl || privacyPolicyUrl;
|
||||
|
||||
const onContinueAsGuestPress = () => {
|
||||
setIsConnecting(true);
|
||||
onChooseWallet(localWallet());
|
||||
setTimeout(() => {
|
||||
onChooseWallet(localWallet());
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const onConnectAWalletPress = () => {
|
||||
setIsConnectAWalletEnabled(true);
|
||||
};
|
||||
|
||||
const onBackPress = () => {
|
||||
setIsConnectAWalletEnabled(false);
|
||||
};
|
||||
|
||||
const onGetStartedPress = () => {
|
||||
Linking.openURL("https://ethereum.org/en/wallets/find-wallet/");
|
||||
};
|
||||
|
||||
const onToSPressed = () => {
|
||||
if (termsOfServiceUrl) {
|
||||
Linking.openURL(termsOfServiceUrl);
|
||||
}
|
||||
};
|
||||
|
||||
const onPrivacyPolicyPress = () => {
|
||||
if (privacyPolicyUrl) {
|
||||
Linking.openURL(privacyPolicyUrl);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Box flexDirection="column">
|
||||
<ModalHeaderTextClose
|
||||
paddingHorizontal="xl"
|
||||
onBackPress={isConnectAWalletEnabled ? onBackPress : undefined}
|
||||
onClose={onClose}
|
||||
headerText={headerText ? headerText : "Choose your wallet"}
|
||||
headerText={
|
||||
headerText ? (
|
||||
headerText
|
||||
) : (
|
||||
<Box
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
alignContent="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<ThirdwebLogo
|
||||
width={26}
|
||||
height={15}
|
||||
color={theme.colors.backgroundInverted}
|
||||
/>
|
||||
<Text
|
||||
variant="header"
|
||||
ml="xxs"
|
||||
fontWeight="700"
|
||||
fontSize={20}
|
||||
lineHeight={24}
|
||||
>
|
||||
Connect
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
subHeaderText={subHeaderText}
|
||||
/>
|
||||
<ChooseWalletContent
|
||||
wallets={wallets}
|
||||
excludeWalletIds={
|
||||
showGuestWalletAsButton
|
||||
? excludeWalletIds
|
||||
: [...excludeWalletIds, walletIds.localWallet]
|
||||
}
|
||||
onChooseWallet={onChooseWallet}
|
||||
/>
|
||||
{guestWallet && !showGuestWalletAsButton ? (
|
||||
<ModalFooter
|
||||
footer={"Continue as Guest"}
|
||||
isLoading={isConnecting}
|
||||
onPress={onContinueAsGuestPress}
|
||||
{!emailWallet || isConnectAWalletEnabled ? (
|
||||
<>
|
||||
<ChooseWalletContent
|
||||
wallets={wallets}
|
||||
excludeWalletIds={[
|
||||
...excludeWalletIds,
|
||||
walletIds.localWallet,
|
||||
walletIds.magicLink,
|
||||
]}
|
||||
onChooseWallet={onChooseWallet}
|
||||
/>
|
||||
|
||||
<Box
|
||||
pt="md"
|
||||
flexGrow={0}
|
||||
borderTopColor="border"
|
||||
borderTopWidth={1}
|
||||
paddingHorizontal="xl"
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text variant="bodySmall">New to wallets?</Text>
|
||||
<BaseButton onPress={onGetStartedPress}>
|
||||
<Text variant="link">Get started</Text>
|
||||
</BaseButton>
|
||||
</Box>
|
||||
</>
|
||||
) : emailWallet.selectUI ? (
|
||||
<emailWallet.selectUI
|
||||
modalSize="compact"
|
||||
theme={themeLightDark}
|
||||
supportedWallets={wallets}
|
||||
onSelect={(data: any) => {
|
||||
onChooseWallet(emailWallet, data);
|
||||
}}
|
||||
walletConfig={emailWallet}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
{emailWallet &&
|
||||
!isConnectAWalletEnabled &&
|
||||
(guestWallet || connectionWallets.length > 0) ? (
|
||||
<Box
|
||||
mb="md"
|
||||
mt="md"
|
||||
marginHorizontal="xl"
|
||||
flexDirection="row"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Box height={1} flex={1} backgroundColor="border" />
|
||||
<Text variant="subHeader" textAlign="center" marginHorizontal="xxs">
|
||||
OR
|
||||
</Text>
|
||||
<Box height={1} flex={1} backgroundColor="border" />
|
||||
</Box>
|
||||
) : null}
|
||||
{emailWallet &&
|
||||
!isConnectAWalletEnabled &&
|
||||
connectionWallets.length > 0 ? (
|
||||
<BaseButton
|
||||
marginHorizontal="xl"
|
||||
justifyContent="center"
|
||||
borderRadius="lg"
|
||||
paddingVertical="xmd"
|
||||
borderColor="border"
|
||||
flexDirection="row"
|
||||
alignItems="center"
|
||||
borderWidth={1}
|
||||
onPress={onConnectAWalletPress}
|
||||
>
|
||||
{connectionWallets.map((wallet) => {
|
||||
return (
|
||||
<Box key={wallet.meta.name} mr="xxs">
|
||||
<ImageSvgUri
|
||||
height={20}
|
||||
width={20}
|
||||
imageUrl={wallet.meta.iconURL}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
<Text variant="bodySmall" fontWeight="700">
|
||||
Connect a wallet
|
||||
</Text>
|
||||
</BaseButton>
|
||||
) : null}
|
||||
{guestWallet ? (
|
||||
<BaseButton
|
||||
marginHorizontal="xl"
|
||||
mt="sm"
|
||||
paddingVertical="xmd"
|
||||
justifyContent="center"
|
||||
borderRadius="lg"
|
||||
borderColor="border"
|
||||
borderWidth={1}
|
||||
onPress={onContinueAsGuestPress}
|
||||
>
|
||||
{isConnecting ? (
|
||||
<ActivityIndicator size={"small"} />
|
||||
) : (
|
||||
<Text variant="bodySmall" fontWeight="700">
|
||||
Continue as guest
|
||||
</Text>
|
||||
)}
|
||||
</BaseButton>
|
||||
) : null}
|
||||
{showToSPrivacyPolicy ? (
|
||||
<Box
|
||||
mt="md"
|
||||
pt="md"
|
||||
height={50}
|
||||
marginHorizontal={!isConnectAWalletEnabled ? "xl" : "none"}
|
||||
alignItems="center"
|
||||
>
|
||||
<Text variant="bodySmallSecondary" fontSize={10} lineHeight={14}>
|
||||
By connecting, you agree to the
|
||||
</Text>
|
||||
<Box flexDirection="row" alignItems="center" justifyContent="center">
|
||||
{termsOfServiceUrl ? (
|
||||
<BaseButton
|
||||
onPress={onToSPressed}
|
||||
flexDirection="row"
|
||||
padding="none"
|
||||
>
|
||||
<Text
|
||||
variant="link"
|
||||
fontSize={10}
|
||||
padding="none"
|
||||
lineHeight={14}
|
||||
>
|
||||
Terms of Service
|
||||
</Text>
|
||||
</BaseButton>
|
||||
) : null}
|
||||
{termsOfServiceUrl && privacyPolicyUrl ? (
|
||||
<Text
|
||||
variant="bodySmallSecondary"
|
||||
fontSize={10}
|
||||
padding="none"
|
||||
lineHeight={14}
|
||||
>
|
||||
{" & "}
|
||||
</Text>
|
||||
) : null}
|
||||
{privacyPolicyUrl ? (
|
||||
<BaseButton
|
||||
onPress={onPrivacyPolicyPress}
|
||||
flexDirection="row"
|
||||
padding="none"
|
||||
>
|
||||
<Text
|
||||
variant="link"
|
||||
fontSize={10}
|
||||
padding="none"
|
||||
lineHeight={14}
|
||||
>
|
||||
Privacy Policy
|
||||
</Text>
|
||||
</BaseButton>
|
||||
) : null}
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
+33
-28
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import type { WalletConfig } from "@thirdweb-dev/react-core";
|
||||
import { StyleSheet, View, FlatList } from "react-native";
|
||||
import { Dimensions, ScrollView, View } from "react-native";
|
||||
import { WalletButton } from "../../base/WalletButton";
|
||||
import Box from "../../base/Box";
|
||||
import { useTheme } from "@shopify/restyle";
|
||||
@@ -11,29 +11,44 @@ interface ChooseWalletContentProps {
|
||||
onChooseWallet: (wallet: WalletConfig, data?: any) => void;
|
||||
}
|
||||
|
||||
const MAX_HEIGHT = Dimensions.get("window").height * 0.3;
|
||||
|
||||
export const ChooseWalletContent = ({
|
||||
wallets,
|
||||
excludeWalletIds,
|
||||
onChooseWallet,
|
||||
}: ChooseWalletContentProps) => {
|
||||
const walletsToDisplay = useMemo(() => {
|
||||
return wallets.filter(
|
||||
(w) => !!!excludeWalletIds?.find((ewId) => ewId === w.id),
|
||||
);
|
||||
}, [wallets, excludeWalletIds]);
|
||||
const theme = useTheme();
|
||||
|
||||
const walletsToDisplay = useMemo(() => {
|
||||
const filteredWallets = wallets.filter(
|
||||
(w) => !!!excludeWalletIds?.find((ewId) => ewId === w.id),
|
||||
);
|
||||
|
||||
const trueItems = filteredWallets.filter(
|
||||
(item) => item.recommended === true,
|
||||
);
|
||||
const falseItems = filteredWallets.filter(
|
||||
(item) => item.recommended !== true,
|
||||
);
|
||||
const sortedWallets = [...trueItems, ...falseItems];
|
||||
|
||||
return sortedWallets;
|
||||
}, [wallets, excludeWalletIds]);
|
||||
|
||||
return (
|
||||
<View style={styles.explorerContainer}>
|
||||
<FlatList
|
||||
keyExtractor={(item) => item.meta.name}
|
||||
data={walletsToDisplay}
|
||||
renderItem={({ item, index }) => {
|
||||
<View style={{ flexDirection: "column", maxHeight: MAX_HEIGHT }}>
|
||||
<ScrollView
|
||||
style={{
|
||||
marginVertical: 16,
|
||||
paddingHorizontal: 16,
|
||||
}}
|
||||
>
|
||||
{walletsToDisplay.map((item, index) => {
|
||||
const marginBottom =
|
||||
index === walletsToDisplay.length - 1 ? "none" : "xxs";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box key={item.id}>
|
||||
{item.selectUI ? (
|
||||
<Box
|
||||
mb={marginBottom}
|
||||
@@ -43,6 +58,7 @@ export const ChooseWalletContent = ({
|
||||
borderRadius="sm"
|
||||
>
|
||||
<item.selectUI
|
||||
modalSize="compact"
|
||||
theme={theme}
|
||||
supportedWallets={wallets}
|
||||
onSelect={(data) => {
|
||||
@@ -55,26 +71,15 @@ export const ChooseWalletContent = ({
|
||||
<WalletButton
|
||||
walletIconUrl={item.meta.iconURL}
|
||||
name={item.meta.name}
|
||||
recommended={item.recommended}
|
||||
onPress={() => onChooseWallet(item)}
|
||||
mb={marginBottom}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
explorerContainer: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 4,
|
||||
marginTop: 25,
|
||||
},
|
||||
});
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { useMemo } from "react";
|
||||
import type { WalletConfig } from "@thirdweb-dev/react-core";
|
||||
import { FlatList, Dimensions } from "react-native";
|
||||
import Box from "../../base/Box";
|
||||
import { useTheme } from "@shopify/restyle";
|
||||
import { WalletButtonSquare } from "../../base/WalletButtonSquare";
|
||||
import Text from "../../base/Text";
|
||||
|
||||
const SCREEN_WIDTH = Dimensions.get("screen").width;
|
||||
const SCREEN_HEIGHT = Dimensions.get("screen").height;
|
||||
const MAX_HEIGHT = SCREEN_HEIGHT * 0.4;
|
||||
const NUM_COLUMNS = 3;
|
||||
const TILE_SIZE = SCREEN_WIDTH / NUM_COLUMNS;
|
||||
|
||||
interface ChooseWalletContentProps {
|
||||
wallets: WalletConfig[];
|
||||
excludeWalletIds?: string[];
|
||||
onChooseWallet: (wallet: WalletConfig, data?: any) => void;
|
||||
}
|
||||
|
||||
export const ChooseWalletContentSquare = ({
|
||||
wallets,
|
||||
excludeWalletIds,
|
||||
onChooseWallet,
|
||||
}: ChooseWalletContentProps) => {
|
||||
const walletsToDisplay = useMemo(() => {
|
||||
return wallets.filter(
|
||||
(w) => !!!excludeWalletIds?.find((ewId) => ewId === w.id),
|
||||
);
|
||||
}, [wallets, excludeWalletIds]);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box marginTop="sm" flexDirection="column">
|
||||
{walletsToDisplay.map((wallet) => {
|
||||
if (!wallet.selectUI) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={wallet.id}
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
borderRadius="sm"
|
||||
>
|
||||
<wallet.selectUI
|
||||
modalSize="compact"
|
||||
theme={theme}
|
||||
supportedWallets={wallets}
|
||||
onSelect={(data) => {
|
||||
onChooseWallet(wallet, data);
|
||||
}}
|
||||
walletConfig={wallet}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
<FlatList
|
||||
keyExtractor={(item) => item.meta.name}
|
||||
data={walletsToDisplay}
|
||||
numColumns={NUM_COLUMNS}
|
||||
style={{ maxHeight: MAX_HEIGHT }}
|
||||
fadingEdgeLength={10}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item }) => {
|
||||
return !item.selectUI ? (
|
||||
<WalletButtonSquare
|
||||
walletIconUrl={item.meta.iconURL}
|
||||
name={item.meta.name}
|
||||
size={TILE_SIZE}
|
||||
onPress={() => onChooseWallet(item)}
|
||||
/>
|
||||
) : null;
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
height={60}
|
||||
borderTopColor="border"
|
||||
borderTopWidth={1}
|
||||
borderBottomColor="border"
|
||||
borderBottomWidth={1}
|
||||
flexDirection="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Text variant="bodySmall">New to wallets?</Text>
|
||||
<Text variant="link">Get started</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { useConnectionStatus, useWallets } from "@thirdweb-dev/react-core";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useModalState } from "../../providers/ui-context-provider";
|
||||
import { ThemeProvider, ThemeProviderProps } from "../../styles/ThemeProvider";
|
||||
import { useAppTheme } from "../../styles/hooks";
|
||||
|
||||
export type ConnectWalletButtonProps = {
|
||||
theme?: ThemeProviderProps["theme"];
|
||||
@@ -17,13 +18,25 @@ export type ConnectWalletButtonProps = {
|
||||
* @default "Choose your wallet"
|
||||
*/
|
||||
modalTitle?: string;
|
||||
/**
|
||||
* Set a custom terms of service url
|
||||
*/
|
||||
termsOfServiceUrl?: string;
|
||||
|
||||
/**
|
||||
* Set a custom privacy policy url
|
||||
*/
|
||||
privacyPolicyUrl?: string;
|
||||
};
|
||||
|
||||
export const ConnectWalletButton = ({
|
||||
modalTitle,
|
||||
termsOfServiceUrl,
|
||||
privacyPolicyUrl,
|
||||
buttonTitle,
|
||||
theme,
|
||||
}: ConnectWalletButtonProps) => {
|
||||
const appTheme = useAppTheme();
|
||||
const connectionStatus = useConnectionStatus();
|
||||
const isWalletConnecting = connectionStatus === "connecting";
|
||||
|
||||
@@ -59,6 +72,8 @@ export const ConnectWalletButton = ({
|
||||
view: "ConnectWalletFlow",
|
||||
data: {
|
||||
modalTitle,
|
||||
termsOfServiceUrl,
|
||||
privacyPolicyUrl,
|
||||
walletConfig:
|
||||
supportedWallets.length === 1 ? supportedWallets[0] : undefined,
|
||||
},
|
||||
@@ -67,7 +82,7 @@ export const ConnectWalletButton = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<ThemeProvider theme={theme ? theme : appTheme}>
|
||||
<BaseButton
|
||||
backgroundColor="buttonBackgroundColor"
|
||||
onPress={onConnectWalletPress}
|
||||
|
||||
@@ -11,11 +11,15 @@ import {
|
||||
CLOSE_MODAL_STATE,
|
||||
ConnectWalletFlowModal,
|
||||
} from "../../utils/modalTypes";
|
||||
import Box from "../base/Box";
|
||||
import { ThemeProvider } from "../../styles/ThemeProvider";
|
||||
import { useAppTheme } from "../../styles/hooks";
|
||||
|
||||
export const ConnectWalletFlow = () => {
|
||||
const { modalState, setModalState } = useModalState();
|
||||
const { modalTitle, walletConfig } = (modalState as ConnectWalletFlowModal)
|
||||
.data;
|
||||
const { modalTitle, privacyPolicyUrl, termsOfServiceUrl, walletConfig } = (
|
||||
modalState as ConnectWalletFlowModal
|
||||
).data;
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [activeWallet, setActiveWallet] = useState<WalletConfig | undefined>();
|
||||
@@ -23,6 +27,7 @@ export const ConnectWalletFlow = () => {
|
||||
const [selectionData, setSelectionData] = useState<any>();
|
||||
const supportedWallets = useWallets();
|
||||
const theme = useColorScheme();
|
||||
const appTheme = useAppTheme();
|
||||
const connect = useConnect();
|
||||
|
||||
const onClose = useCallback(
|
||||
@@ -67,7 +72,7 @@ export const ConnectWalletFlow = () => {
|
||||
|
||||
useEffect(() => {
|
||||
// case when only one wallet is passed in supportedWallets
|
||||
if (walletConfig) {
|
||||
if (walletConfig && !walletConfig.connectUI) {
|
||||
onChooseWallet(walletConfig);
|
||||
}
|
||||
}, [onChooseWallet, walletConfig]);
|
||||
@@ -102,6 +107,7 @@ export const ConnectWalletFlow = () => {
|
||||
if (activeWallet?.connectUI) {
|
||||
return (
|
||||
<activeWallet.connectUI
|
||||
modalSize="compact"
|
||||
theme={theme || "dark"}
|
||||
goBack={onBackPress}
|
||||
close={handleClose}
|
||||
@@ -127,32 +133,37 @@ export const ConnectWalletFlow = () => {
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{activeWallet ? (
|
||||
isConnecting ? (
|
||||
<ConnectingWallet
|
||||
content={
|
||||
activeWallet.id === walletIds.localWallet ? (
|
||||
<Text variant="bodySmallSecondary" mt="md">
|
||||
Creating, encrypting and securing your device wallet.
|
||||
</Text>
|
||||
) : undefined
|
||||
}
|
||||
wallet={activeWallet}
|
||||
onClose={onClose}
|
||||
onBackPress={onBackPress}
|
||||
/>
|
||||
<ThemeProvider theme={appTheme}>
|
||||
<Box flexDirection="column">
|
||||
{activeWallet ? (
|
||||
isConnecting ? (
|
||||
<ConnectingWallet
|
||||
subHeaderText=""
|
||||
content={
|
||||
activeWallet.id === walletIds.localWallet ? (
|
||||
<Text variant="bodySmallSecondary" mt="md">
|
||||
Creating, encrypting and securing your device wallet.
|
||||
</Text>
|
||||
) : undefined
|
||||
}
|
||||
wallet={activeWallet}
|
||||
onClose={onClose}
|
||||
onBackPress={onBackPress}
|
||||
/>
|
||||
) : (
|
||||
getComponentForWallet()
|
||||
)
|
||||
) : (
|
||||
getComponentForWallet()
|
||||
)
|
||||
) : (
|
||||
<ChooseWallet
|
||||
headerText={modalTitle}
|
||||
wallets={supportedWallets}
|
||||
onChooseWallet={onChooseWallet}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
<ChooseWallet
|
||||
headerText={modalTitle}
|
||||
privacyPolicyUrl={privacyPolicyUrl}
|
||||
termsOfServiceUrl={termsOfServiceUrl}
|
||||
wallets={supportedWallets}
|
||||
onChooseWallet={onChooseWallet}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
+19
-27
@@ -1,10 +1,10 @@
|
||||
import { useAppTheme } from "../../../styles/hooks";
|
||||
import WalletLoadingThumbnail from "../../../wallets/wallets/wallet-connect/WalletLoadingThumbnail";
|
||||
import { Box, ImageSvgUri } from "../../base";
|
||||
import Text from "../../base/Text";
|
||||
import { ModalFooter } from "../../base/modal/ModalFooter";
|
||||
import { ConnectWalletHeader } from "./ConnectingWalletHeader";
|
||||
import type { WalletConfig } from "@thirdweb-dev/react-core";
|
||||
import { ReactNode } from "react";
|
||||
import { ActivityIndicator, Linking, StyleSheet, View } from "react-native";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
|
||||
export type ConnectingWalletProps = {
|
||||
subHeaderText?: string;
|
||||
@@ -12,7 +12,7 @@ export type ConnectingWalletProps = {
|
||||
content?: ReactNode;
|
||||
onClose: () => void;
|
||||
onBackPress: () => void;
|
||||
wallet: WalletConfig;
|
||||
wallet: WalletConfig<any>;
|
||||
};
|
||||
|
||||
export function ConnectingWallet({
|
||||
@@ -23,51 +23,43 @@ export function ConnectingWallet({
|
||||
onClose,
|
||||
onBackPress,
|
||||
}: ConnectingWalletProps) {
|
||||
const theme = useAppTheme();
|
||||
|
||||
const onFooterPress = () => {
|
||||
Linking.openURL("https://support.thirdweb.com/");
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Box paddingHorizontal="xl">
|
||||
<ConnectWalletHeader
|
||||
onBackPress={onBackPress}
|
||||
walletLogoUrl={wallet.meta.iconURL}
|
||||
subHeaderText={subHeaderText}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<WalletLoadingThumbnail imageSize={80}>
|
||||
<ImageSvgUri height={80} width={80} imageUrl={wallet.meta.iconURL} />
|
||||
</WalletLoadingThumbnail>
|
||||
<View style={styles.connectingContainer}>
|
||||
<ActivityIndicator size="small" color={theme.colors.linkPrimary} />
|
||||
{content ? (
|
||||
content
|
||||
) : (
|
||||
<Text variant="bodySmallSecondary" mt="md">
|
||||
Connect your wallet through the {wallet.meta.name} application.
|
||||
</Text>
|
||||
<>
|
||||
<Text variant="header" mt="lg">
|
||||
Connecting your wallet
|
||||
</Text>
|
||||
<Text variant="bodySmallSecondary" mt="lg" textAlign="center">
|
||||
Login and connect your wallet through the metamask pop-up
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
{footer ? (
|
||||
footer
|
||||
) : (
|
||||
<ModalFooter
|
||||
footer={`Having troubles connecting to ${wallet.meta.name}?`}
|
||||
onPress={onFooterPress}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
{footer ? footer : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
connectingContainer: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "center",
|
||||
alignContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 4,
|
||||
marginBottom: 24,
|
||||
marginTop: 18,
|
||||
},
|
||||
});
|
||||
|
||||
+1
-3
@@ -33,9 +33,7 @@ export const ConnectWalletHeader = ({
|
||||
onPress={onBackPress}
|
||||
color={theme.colors.iconPrimary}
|
||||
/>
|
||||
) : (
|
||||
<View />
|
||||
)}
|
||||
) : null}
|
||||
{walletLogoUrl ? (
|
||||
<ImageSvgUri width={56} height={56} imageUrl={walletLogoUrl} />
|
||||
) : null}
|
||||
|
||||
+6
-4
@@ -196,10 +196,12 @@ export const LocalWalletImportModal = ({
|
||||
---- Or Private key or Mnemonic ----
|
||||
</Text>
|
||||
<TextInput
|
||||
secureTextEntry={true}
|
||||
placeholder={"Private key / Mnemonic"}
|
||||
placeholderTextColor={theme.colors.textSecondary}
|
||||
onChangeText={onPrivateKeyEntered}
|
||||
textInputProps={{
|
||||
secureTextEntry: true,
|
||||
placeholder: "Private key / Mnemonic",
|
||||
placeholderTextColor: theme.colors.textSecondary,
|
||||
onChangeText: onPrivateKeyEntered,
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
flexDirection="row"
|
||||
|
||||
+3
-1
@@ -137,6 +137,7 @@ export const SmartWalletFlow = ({
|
||||
return (
|
||||
<>
|
||||
<ModalHeaderTextClose
|
||||
marginHorizontal="xl"
|
||||
onClose={onConnectingClosePress}
|
||||
headerText={mismatch ? "Network Mismatch" : "Connecting smart wallet"}
|
||||
subHeaderText={
|
||||
@@ -172,6 +173,7 @@ export const SmartWalletFlow = ({
|
||||
if (showLocalWalletFlow) {
|
||||
return (
|
||||
<LocalWalletFlow
|
||||
modalSize="compact"
|
||||
theme={colorScheme || "dark"}
|
||||
close={onClose}
|
||||
goBack={onLocalWalletBackPress}
|
||||
@@ -190,7 +192,7 @@ export const SmartWalletFlow = ({
|
||||
<ChooseWallet
|
||||
headerText={"Link key"}
|
||||
subHeaderText={
|
||||
<Text variant="subHeader">
|
||||
<Text variant="subHeader" marginHorizontal="xl">
|
||||
{
|
||||
"Choose a personal wallet that acts as your account's key. This controls access to your account. "
|
||||
}
|
||||
|
||||
@@ -4,17 +4,15 @@ import { Dimensions, StyleSheet, View } from "react-native";
|
||||
import { useMemo } from "react";
|
||||
import { CLOSE_MODAL_STATE } from "../utils/modalTypes";
|
||||
import { ThemeProvider } from "../styles/ThemeProvider";
|
||||
import { useAppTheme } from "../styles/hooks";
|
||||
import { SessionRequestModal } from "./ConnectWalletDetails/SessionRequestModal";
|
||||
import { SessionProposalModal } from "./ConnectWalletDetails/SessionProposalModal";
|
||||
import { TWModal } from "./base/modal/TWModal";
|
||||
import Box from "./base/Box";
|
||||
|
||||
const MODAL_HEIGHT = Dimensions.get("window").height * 0.7;
|
||||
const MODAL_HEIGHT = Dimensions.get("window").height * 0.6;
|
||||
const DEVICE_WIDTH = Dimensions.get("window").width;
|
||||
|
||||
export const MainModal = () => {
|
||||
const theme = useAppTheme();
|
||||
|
||||
const { modalState, setModalState } = useModalState();
|
||||
|
||||
const { isOpen, isSheet } = modalState;
|
||||
@@ -39,14 +37,12 @@ export const MainModal = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<ThemeProvider>
|
||||
<TWModal isVisible={isOpen} onBackdropPress={onBackdropPress}>
|
||||
{isSheet ? (
|
||||
<View
|
||||
style={[styles.modal, { backgroundColor: theme.colors.background }]}
|
||||
>
|
||||
<Box backgroundColor="background" style={styles.modal}>
|
||||
<View style={styles.contentContainer}>{view}</View>
|
||||
</View>
|
||||
</Box>
|
||||
) : (
|
||||
view
|
||||
)}
|
||||
@@ -63,8 +59,8 @@ const styles = StyleSheet.create({
|
||||
width: DEVICE_WIDTH,
|
||||
borderTopLeftRadius: 24,
|
||||
borderTopRightRadius: 24,
|
||||
padding: 24,
|
||||
paddingBottom: 40,
|
||||
paddingTop: 32,
|
||||
paddingBottom: 28,
|
||||
},
|
||||
contentContainer: {
|
||||
maxHeight: MODAL_HEIGHT,
|
||||
|
||||
@@ -31,6 +31,8 @@ export const PasswordInput = ({
|
||||
secureTextEntry={!showPassword}
|
||||
textContentType="none"
|
||||
returnKeyType={"done"}
|
||||
placeholder="Password"
|
||||
placeholderTextColor={theme.colors.textSecondary}
|
||||
clearTextOnFocus={false}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { CallOverrides, ContractInterface } from "ethers";
|
||||
import { PropsWithChildren, useEffect } from "react";
|
||||
import { ActivityIndicator, StyleSheet } from "react-native";
|
||||
import invariant from "tiny-invariant";
|
||||
import { useUIContext } from "../providers/ui-context-provider";
|
||||
|
||||
type ActionFn = (contract: SmartContract) => Promise<any>;
|
||||
|
||||
@@ -82,11 +83,18 @@ export const Web3Button = <TAction extends ActionFn>({
|
||||
const needToSwitchChain =
|
||||
sdkChainId && walletChainId && sdkChainId !== walletChainId;
|
||||
const connectionStatus = useConnectionStatus();
|
||||
const setTheme = useUIContext().setTheme;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { contract } = useContract(contractAddress, contractAbi || "custom");
|
||||
|
||||
useEffect(() => {
|
||||
if (theme) {
|
||||
setTheme(theme);
|
||||
}
|
||||
}, [setTheme, theme]);
|
||||
|
||||
const actionMutation = useMutation(
|
||||
async () => {
|
||||
invariant(contract, "contract is not ready yet");
|
||||
@@ -153,7 +161,7 @@ export const Web3Button = <TAction extends ActionFn>({
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<ThemeProvider>
|
||||
<BaseButton
|
||||
backgroundColor="buttonBackgroundColor"
|
||||
onPress={() => {
|
||||
|
||||
@@ -2,12 +2,14 @@ import { StyleSheet, TextInput as TextInputRN } from "react-native";
|
||||
import Box from "./Box";
|
||||
import { useTheme } from "@shopify/restyle";
|
||||
|
||||
type TextInputProps = (typeof Box)["arguments"] & TextInputRN["props"];
|
||||
type TextInputProps = {
|
||||
containerProps?: (typeof Box)["arguments"];
|
||||
textInputProps?: TextInputRN["props"];
|
||||
};
|
||||
|
||||
export const TextInput = ({
|
||||
onChangeText,
|
||||
placeholder,
|
||||
...props
|
||||
containerProps,
|
||||
textInputProps,
|
||||
}: TextInputProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -19,17 +21,15 @@ export const TextInput = ({
|
||||
borderWidth={1}
|
||||
borderRadius="md"
|
||||
pr="xs"
|
||||
{...props}
|
||||
{...containerProps}
|
||||
>
|
||||
<TextInputRN
|
||||
style={{ ...styles.textInput, color: theme.colors.textPrimary }}
|
||||
returnKeyType={"done"}
|
||||
clearTextOnFocus={false}
|
||||
placeholder={placeholder}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
onChangeText={onChangeText}
|
||||
{...props}
|
||||
{...textInputProps}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ type WalletButtonProps = {
|
||||
walletIconUrl: string;
|
||||
name: string;
|
||||
labelText?: string;
|
||||
recommended?: boolean;
|
||||
} & React.ComponentProps<typeof BaseButton>;
|
||||
|
||||
export const WalletButton = ({
|
||||
@@ -18,6 +19,7 @@ export const WalletButton = ({
|
||||
walletIconUrl,
|
||||
name,
|
||||
labelText,
|
||||
recommended,
|
||||
}: WalletButtonProps) => {
|
||||
return (
|
||||
<BaseButton
|
||||
@@ -28,14 +30,15 @@ export const WalletButton = ({
|
||||
paddingHorizontal="md"
|
||||
paddingVertical="sm"
|
||||
borderRadius="sm"
|
||||
backgroundColor="backgroundHighlight"
|
||||
backgroundColor="background"
|
||||
onPress={onPress}
|
||||
>
|
||||
<Box flexDirection="row" justifyContent="flex-start" alignItems="center">
|
||||
<ImageSvgUri imageUrl={walletIconUrl} width={32} height={32} />
|
||||
<Text ml="md" variant="bodyLarge">
|
||||
{name}
|
||||
</Text>
|
||||
<ImageSvgUri imageUrl={walletIconUrl} width={48} height={48} />
|
||||
<Box ml="sm" alignItems="flex-start">
|
||||
<Text variant="bodyLarge">{name}</Text>
|
||||
{recommended ? <Text variant="link">Recommended</Text> : null}
|
||||
</Box>
|
||||
</Box>
|
||||
{labelText ? <Label text={labelText} /> : <View />}
|
||||
</BaseButton>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useAppTheme } from "../../styles/hooks";
|
||||
import BaseButton from "./BaseButton";
|
||||
import Box from "./Box";
|
||||
import ImageSvgUri from "./ImageSvgUri";
|
||||
import Text from "./Text";
|
||||
|
||||
type WalletButtonSquareProps = {
|
||||
onPress: () => void;
|
||||
walletIconUrl: string;
|
||||
size: number;
|
||||
name: string;
|
||||
} & (typeof BaseButton)["arguments"];
|
||||
|
||||
export const WalletButtonSquare = ({
|
||||
onPress,
|
||||
walletIconUrl,
|
||||
name,
|
||||
size,
|
||||
}: WalletButtonSquareProps) => {
|
||||
const theme = useAppTheme();
|
||||
|
||||
const marginVertical = theme.spacing.xs;
|
||||
return (
|
||||
<BaseButton
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
marginVertical="xxs"
|
||||
borderWidth={0}
|
||||
borderRadius="sm"
|
||||
width={size - marginVertical * 2}
|
||||
height={size - marginVertical * 2}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Box borderWidth={0} borderRadius="lg">
|
||||
<ImageSvgUri
|
||||
imageUrl={walletIconUrl}
|
||||
width={size * 0.6}
|
||||
height={size * 0.6}
|
||||
/>
|
||||
</Box>
|
||||
<Text
|
||||
variant="bodySmallSecondary"
|
||||
numberOfLines={1}
|
||||
mt="xs"
|
||||
fontSize={14}
|
||||
lineHeight={16}
|
||||
>
|
||||
{name}
|
||||
</Text>
|
||||
</BaseButton>
|
||||
);
|
||||
};
|
||||
@@ -8,12 +8,14 @@ interface ModalHeaderTextCloseProps {
|
||||
onClose: () => void;
|
||||
headerText?: ReactNode | string;
|
||||
subHeaderText?: ReactNode | string;
|
||||
onBackPress?: () => void;
|
||||
}
|
||||
|
||||
export const ModalHeaderTextClose = ({
|
||||
headerText,
|
||||
subHeaderText,
|
||||
onClose,
|
||||
onBackPress,
|
||||
...props
|
||||
}: ModalHeaderTextCloseProps & React.ComponentProps<typeof Box>) => {
|
||||
const theme = useAppTheme();
|
||||
@@ -25,6 +27,15 @@ export const ModalHeaderTextClose = ({
|
||||
justifyContent={headerText ? "space-between" : "flex-end"}
|
||||
{...props}
|
||||
>
|
||||
{onBackPress ? (
|
||||
<Icon
|
||||
type="back"
|
||||
width={16}
|
||||
height={16}
|
||||
onPress={onBackPress}
|
||||
color={theme.colors.iconPrimary}
|
||||
/>
|
||||
) : null}
|
||||
{typeof headerText === "string" ? (
|
||||
<Text variant="header">{headerText}</Text>
|
||||
) : (
|
||||
@@ -32,8 +43,8 @@ export const ModalHeaderTextClose = ({
|
||||
)}
|
||||
<Icon
|
||||
type="close"
|
||||
width={14}
|
||||
height={14}
|
||||
width={16}
|
||||
height={16}
|
||||
color={theme.colors.iconSecondary}
|
||||
onPress={onClose}
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ThemeProvider } from "../styles/ThemeProvider";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { walletIds } from "@thirdweb-dev/wallets";
|
||||
import { ThirdwebStorage } from "../../core/storage/storage";
|
||||
import { useColorScheme } from "react-native";
|
||||
|
||||
interface ThirdwebProviderProps<TChains extends Chain[]>
|
||||
extends Omit<
|
||||
@@ -70,6 +71,7 @@ export const ThirdwebProvider = <
|
||||
sdkOptions,
|
||||
...restProps
|
||||
}: PropsWithChildren<ThirdwebProviderProps<TChains>>) => {
|
||||
const colorScheme = useColorScheme();
|
||||
useCoinbaseWalletListener(
|
||||
!!supportedWallets.find((w) => w.id === walletIds.coinbase),
|
||||
);
|
||||
@@ -102,7 +104,9 @@ export const ThirdwebProvider = <
|
||||
{...sdkOptions}
|
||||
{...restProps}
|
||||
>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ThemeProvider
|
||||
theme={theme ? theme : colorScheme === "dark" ? "dark" : "light"}
|
||||
>
|
||||
<UIContextProvider>
|
||||
{hasMagicConfig ? (
|
||||
<SafeAreaProvider>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createContext, useContext, useMemo, useState } from "react";
|
||||
import { ModalState } from "../utils/modalTypes";
|
||||
import { ThemeProviderProps } from "../styles/ThemeProvider";
|
||||
import { Theme, darkTheme, lightTheme } from "../styles/theme";
|
||||
import { Theme, _darkTheme, _lightTheme } from "../styles/theme";
|
||||
import { useAppTheme } from "../styles/hooks";
|
||||
import { useTheme } from "@shopify/restyle";
|
||||
import { PropsWithChildren } from "react";
|
||||
@@ -22,7 +22,7 @@ const UIContext = createContext<UIContextType>({
|
||||
caller: "init",
|
||||
},
|
||||
setModalState: () => undefined,
|
||||
theme: darkTheme(),
|
||||
theme: "light",
|
||||
setTheme: () => undefined,
|
||||
});
|
||||
|
||||
@@ -65,9 +65,9 @@ export const useModalState = (): {
|
||||
|
||||
const getThemeObj = (theme?: ThemeProviderProps["theme"]) => {
|
||||
if (theme === "dark" || !theme) {
|
||||
return darkTheme();
|
||||
return _darkTheme;
|
||||
} else if (theme === "light") {
|
||||
return lightTheme();
|
||||
return _lightTheme;
|
||||
} else {
|
||||
return theme;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ThemeProvider as ShopifyThemeProvider } from "@shopify/restyle";
|
||||
import { ThirdwebThemeContext } from "@thirdweb-dev/react-core";
|
||||
import { PropsWithChildren, useContext } from "react";
|
||||
import { useColorScheme } from "react-native";
|
||||
import { useUIContext } from "../providers/ui-context-provider";
|
||||
|
||||
export type ThemeProviderProps = {
|
||||
theme?: Theme | "light" | "dark";
|
||||
@@ -14,7 +15,9 @@ export function ThemeProvider({
|
||||
}: PropsWithChildren<ThemeProviderProps>) {
|
||||
const isDarkMode = useColorScheme() === "dark";
|
||||
const themeFromCore = useContext(ThirdwebThemeContext);
|
||||
const theme_ = theme || themeFromCore || (isDarkMode ? "dark" : "light");
|
||||
const themeFromRN = useUIContext().theme;
|
||||
const theme_ =
|
||||
theme || themeFromRN || themeFromCore || (isDarkMode ? "dark" : "light");
|
||||
return (
|
||||
<ShopifyThemeProvider
|
||||
theme={
|
||||
|
||||
@@ -2,6 +2,10 @@ export type Palette = {
|
||||
buttonBackgroundColor: string;
|
||||
buttonTextColor: string;
|
||||
|
||||
accentButtonColor: string;
|
||||
accentButtonTextColor: string;
|
||||
|
||||
backgroundInverted: string;
|
||||
background: string;
|
||||
backgroundHighlight: string;
|
||||
|
||||
@@ -39,6 +43,10 @@ export const paletteLight: Palette = {
|
||||
buttonBackgroundColor: "white",
|
||||
buttonTextColor: "black",
|
||||
|
||||
accentButtonColor: "#3385FF",
|
||||
accentButtonTextColor: "white",
|
||||
|
||||
backgroundInverted: "#131417",
|
||||
background: "#F7F7F9",
|
||||
backgroundHighlight: "#FFFFFF",
|
||||
|
||||
@@ -58,9 +66,13 @@ export const paletteLight: Palette = {
|
||||
};
|
||||
|
||||
export const paletteDark: Palette = {
|
||||
buttonBackgroundColor: "white",
|
||||
buttonTextColor: "black",
|
||||
buttonBackgroundColor: "black",
|
||||
buttonTextColor: "white",
|
||||
|
||||
accentButtonColor: "#3385FF",
|
||||
accentButtonTextColor: "white",
|
||||
|
||||
backgroundInverted: "white",
|
||||
background: "#131417",
|
||||
backgroundHighlight: "#232429",
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DeepPartial } from "../types/deepPartial";
|
||||
import { paletteDark, paletteLight } from "./colors";
|
||||
import { textVariants } from "./textVariants";
|
||||
import { createTheme } from "@shopify/restyle";
|
||||
@@ -9,17 +10,11 @@ export const baseTheme = {
|
||||
xs: 8,
|
||||
sm: 12,
|
||||
md: 16,
|
||||
xmd: 20,
|
||||
lg: 24,
|
||||
xl: 32,
|
||||
xxl: 48,
|
||||
},
|
||||
fontSize: {
|
||||
xs: 12,
|
||||
sm: 14,
|
||||
md: 16,
|
||||
lg: 20,
|
||||
xl: 24,
|
||||
},
|
||||
borderRadii: {
|
||||
xs: 4,
|
||||
sm: 6,
|
||||
@@ -28,30 +23,24 @@ export const baseTheme = {
|
||||
xl: 20,
|
||||
xxl: 32,
|
||||
},
|
||||
iconSize: {
|
||||
sm: 16,
|
||||
md: 24,
|
||||
lg: 32,
|
||||
xl: 48,
|
||||
},
|
||||
textVariants,
|
||||
};
|
||||
|
||||
const darkTheme_ = createTheme({
|
||||
export const _darkTheme = createTheme({
|
||||
...baseTheme,
|
||||
colors: {
|
||||
...paletteDark,
|
||||
},
|
||||
});
|
||||
|
||||
const lightTheme_: Theme = {
|
||||
export const _lightTheme: Theme = {
|
||||
...baseTheme,
|
||||
colors: {
|
||||
...paletteLight,
|
||||
},
|
||||
};
|
||||
|
||||
export type Theme = typeof darkTheme_;
|
||||
export type Theme = typeof _darkTheme;
|
||||
|
||||
export type ButtonTheme = {
|
||||
buttonBackgroundColor?: string;
|
||||
@@ -61,27 +50,35 @@ export type ButtonTheme = {
|
||||
export const darkTheme = ({
|
||||
buttonBackgroundColor = paletteDark.buttonBackgroundColor,
|
||||
buttonTextColor = paletteDark.buttonTextColor,
|
||||
}: ButtonTheme = {}) => {
|
||||
colors,
|
||||
...props
|
||||
}: ButtonTheme & DeepPartial<Theme> = _darkTheme): Theme => {
|
||||
return {
|
||||
...darkTheme_,
|
||||
..._darkTheme,
|
||||
colors: {
|
||||
...darkTheme_.colors,
|
||||
..._darkTheme.colors,
|
||||
...colors,
|
||||
buttonBackgroundColor,
|
||||
buttonTextColor,
|
||||
},
|
||||
};
|
||||
...props,
|
||||
} as Theme;
|
||||
};
|
||||
|
||||
export const lightTheme = ({
|
||||
buttonBackgroundColor = paletteLight.buttonBackgroundColor,
|
||||
buttonTextColor = paletteLight.buttonTextColor,
|
||||
}: ButtonTheme = {}) => {
|
||||
colors,
|
||||
...props
|
||||
}: ButtonTheme & DeepPartial<Theme> = _lightTheme): Theme => {
|
||||
return {
|
||||
...lightTheme_,
|
||||
..._lightTheme,
|
||||
colors: {
|
||||
...lightTheme_.colors,
|
||||
..._lightTheme.colors,
|
||||
...colors,
|
||||
buttonBackgroundColor,
|
||||
buttonTextColor,
|
||||
},
|
||||
};
|
||||
...props,
|
||||
} as Theme;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
};
|
||||
@@ -52,6 +52,8 @@ export type ClosedModal = {
|
||||
// connect wallet flow
|
||||
export type ConnectWalletFlowData = {
|
||||
modalTitle?: string;
|
||||
termsOfServiceUrl?: string;
|
||||
privacyPolicyUrl?: string;
|
||||
walletConfig?: WalletConfig;
|
||||
};
|
||||
|
||||
|
||||
@@ -69,7 +69,10 @@ export class CoinbaseWallet extends AbstractClientWallet<CoinbaseWalletConnector
|
||||
}
|
||||
}
|
||||
|
||||
export const coinbaseWallet = (config?: { callbackURL?: URL }) => {
|
||||
export const coinbaseWallet = (config?: {
|
||||
callbackURL?: URL;
|
||||
recommended?: boolean;
|
||||
}) => {
|
||||
const callbackURLNonNull =
|
||||
config?.callbackURL || new URL("https://thirdweb.com/wsegue");
|
||||
return {
|
||||
@@ -77,5 +80,6 @@ export const coinbaseWallet = (config?: { callbackURL?: URL }) => {
|
||||
meta: CoinbaseWallet.meta,
|
||||
create: (options: WalletOptionsRC) =>
|
||||
new CoinbaseWallet({ ...options, callbackURL: callbackURLNonNull }),
|
||||
recommended: config?.recommended,
|
||||
} satisfies WalletConfig<CoinbaseWallet>;
|
||||
};
|
||||
|
||||
@@ -15,11 +15,14 @@ import { useEffect, useRef } from "react";
|
||||
import Box from "../../components/base/Box";
|
||||
import { TextInput } from "../../components/base/TextInput";
|
||||
import Text from "../../components/base/Text";
|
||||
import { ActivityIndicator } from "react-native";
|
||||
import { useMagicLink } from "../../providers/context-provider";
|
||||
import BaseButton from "../../components/base/BaseButton";
|
||||
import { useAppTheme } from "../../styles/hooks";
|
||||
import React from "react";
|
||||
import { ConnectingWallet } from "../../components/ConnectWalletFlow/ConnectingWallet/ConnectingWallet";
|
||||
|
||||
export const magicLink = (
|
||||
magicLinkOptions: MagicLinkOptions,
|
||||
magicLinkOptions: MagicLinkOptions & { recommended?: boolean },
|
||||
): WalletConfig<MagicLink> => {
|
||||
return {
|
||||
id: walletIds.magicLink,
|
||||
@@ -42,35 +45,70 @@ export const magicLink = (
|
||||
/**
|
||||
* UI for selecting wallet - this UI is rendered in the wallet selection screen
|
||||
*/
|
||||
selectUI: (props: SelectUIProps<MagicLink>) => {
|
||||
return (
|
||||
<Box flex={1}>
|
||||
<TextInput
|
||||
placeholder="Enter your email or phone number"
|
||||
placeholderTextColor="gray"
|
||||
onEndEditing={(e: any) => {
|
||||
props.onSelect(e.nativeEvent.text);
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
marginVertical="sm"
|
||||
variant="bodySmallSecondary"
|
||||
textAlign="center"
|
||||
>
|
||||
---- OR ----
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
selectUI: MagicSelectionUI,
|
||||
isInstalled: () => {
|
||||
return true;
|
||||
},
|
||||
recommended: magicLinkOptions?.recommended,
|
||||
};
|
||||
};
|
||||
|
||||
const MagicSelectionUI: React.FC<SelectUIProps<MagicLink>> = (props) => {
|
||||
const theme = useAppTheme();
|
||||
const [email, setEmail] = React.useState("");
|
||||
|
||||
const onContinuePress = () => {
|
||||
if (!email) {
|
||||
return;
|
||||
}
|
||||
props.onSelect(email);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box paddingHorizontal="xl" mt="lg">
|
||||
<TextInput
|
||||
textInputProps={{
|
||||
placeholder: "Enter your email address",
|
||||
placeholderTextColor: theme.colors.textSecondary,
|
||||
onChangeText: (text: string) => {
|
||||
setEmail(text);
|
||||
},
|
||||
style: {
|
||||
fontSize: 14,
|
||||
color: theme.colors.textPrimary,
|
||||
lineHeight: 16,
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
containerProps={{
|
||||
paddingHorizontal: "sm",
|
||||
paddingVertical: "md",
|
||||
}}
|
||||
/>
|
||||
<BaseButton
|
||||
mt="md"
|
||||
paddingVertical="xmd"
|
||||
borderRadius="lg"
|
||||
borderWidth={1}
|
||||
borderColor="border"
|
||||
backgroundColor="accentButtonColor"
|
||||
onPress={onContinuePress}
|
||||
>
|
||||
<Text
|
||||
variant="bodySmall"
|
||||
color="accentButtonTextColor"
|
||||
fontWeight="700"
|
||||
>
|
||||
Continue
|
||||
</Text>
|
||||
</BaseButton>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const MagicConnectionUI: React.FC<
|
||||
ConnectUIProps<MagicLink> & { magicLinkOptions: MagicLinkOptions }
|
||||
> = ({ selectionData, walletConfig, close, magicLinkOptions }) => {
|
||||
> = ({ selectionData, walletConfig, close, goBack, magicLinkOptions }) => {
|
||||
const createWalletInstance = useCreateWalletInstance();
|
||||
const setConnectedWallet = useSetConnectedWallet();
|
||||
const chainToConnect = useWalletContext().chainToConnect;
|
||||
@@ -125,10 +163,11 @@ const MagicConnectionUI: React.FC<
|
||||
]);
|
||||
|
||||
return (
|
||||
<Box minHeight={500}>
|
||||
<ActivityIndicator size="small" />
|
||||
<TextInput placeholder="Enter the OTP sent to your email / phone number" />
|
||||
</Box>
|
||||
<ConnectingWallet
|
||||
wallet={walletConfig}
|
||||
onClose={close}
|
||||
onBackPress={goBack}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { walletIds } from "@thirdweb-dev/wallets";
|
||||
import { WalletOptions, WalletConfig } from "@thirdweb-dev/react-core";
|
||||
import { WCMeta } from "../types/wc";
|
||||
import { WalletConnectBase } from "./wallet-connect/WalletConnectBase";
|
||||
import { WalletConnectConfig } from "./wallet-connect/wallet-connect";
|
||||
|
||||
export class MetaMaskWallet extends WalletConnectBase {
|
||||
static id = walletIds.metamask;
|
||||
@@ -20,9 +21,7 @@ export class MetaMaskWallet extends WalletConnectBase {
|
||||
}
|
||||
}
|
||||
|
||||
type MetaMaskWalletConfig = { projectId?: string };
|
||||
|
||||
export const metamaskWallet = (config?: MetaMaskWalletConfig) => {
|
||||
export const metamaskWallet = (config?: WalletConnectConfig) => {
|
||||
return {
|
||||
id: MetaMaskWallet.id,
|
||||
meta: MetaMaskWallet.meta,
|
||||
@@ -32,5 +31,6 @@ export const metamaskWallet = (config?: MetaMaskWalletConfig) => {
|
||||
walletId: walletIds.metamask,
|
||||
projectId: config?.projectId,
|
||||
}),
|
||||
recommended: config?.recommended,
|
||||
} satisfies WalletConfig;
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
WalletConfig,
|
||||
} from "@thirdweb-dev/react-core";
|
||||
import { WalletConnectBase } from "./wallet-connect/WalletConnectBase";
|
||||
import { WalletConnectConfig } from "./wallet-connect/wallet-connect";
|
||||
|
||||
export class RainbowWallet extends WalletConnectBase {
|
||||
static id = "rainbow" as const;
|
||||
@@ -21,9 +22,7 @@ export class RainbowWallet extends WalletConnectBase {
|
||||
}
|
||||
}
|
||||
|
||||
type RainbowWalletConfig = { projectId?: string };
|
||||
|
||||
export const rainbowWallet = (config?: RainbowWalletConfig) => {
|
||||
export const rainbowWallet = (config?: WalletConnectConfig) => {
|
||||
return {
|
||||
id: RainbowWallet.id,
|
||||
meta: RainbowWallet.meta,
|
||||
@@ -33,5 +32,6 @@ export const rainbowWallet = (config?: RainbowWalletConfig) => {
|
||||
walletId: RainbowWallet.id,
|
||||
projectId: config?.projectId,
|
||||
}),
|
||||
recommended: config?.recommended,
|
||||
} satisfies WalletConfig;
|
||||
};
|
||||
|
||||
@@ -9,7 +9,8 @@ import { createSyncStorage } from "../../../core/AsyncStorage";
|
||||
|
||||
type SmartWalletConfig = {
|
||||
personalWallets?: WalletConfig<any>[];
|
||||
} & Omit<SmartWalletConfigWallets, "chain" | 'clientId' | 'secretKey'>;
|
||||
recommended?: boolean;
|
||||
} & Omit<SmartWalletConfigWallets, "chain" | "clientId" | "secretKey">;
|
||||
|
||||
export type SmartWalletObj = WalletConfig<SmartWallet>;
|
||||
|
||||
@@ -27,5 +28,6 @@ export const smartWallet = (
|
||||
wcStorage: createSyncStorage("smart-wallet"),
|
||||
}),
|
||||
personalWallets: config.personalWallets || DEFAULT_WALLETS,
|
||||
recommended: config.recommended,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { WalletOptions, WalletConfig } from "@thirdweb-dev/react-core";
|
||||
import { WCMeta } from "../types/wc";
|
||||
import { WalletConnectBase } from "./wallet-connect/WalletConnectBase";
|
||||
import { WalletConnectConfig } from "./wallet-connect/wallet-connect";
|
||||
|
||||
export class TrustWallet extends WalletConnectBase {
|
||||
static id = "trust" as const;
|
||||
@@ -19,9 +20,7 @@ export class TrustWallet extends WalletConnectBase {
|
||||
}
|
||||
}
|
||||
|
||||
type TrustWalletConfig = { projectId?: string };
|
||||
|
||||
export const trustWallet = (config?: TrustWalletConfig) => {
|
||||
export const trustWallet = (config?: WalletConnectConfig) => {
|
||||
return {
|
||||
id: TrustWallet.id,
|
||||
meta: TrustWallet.meta,
|
||||
@@ -31,5 +30,6 @@ export const trustWallet = (config?: TrustWalletConfig) => {
|
||||
walletId: "trust",
|
||||
projectId: config?.projectId,
|
||||
}),
|
||||
recommended: config?.recommended,
|
||||
} satisfies WalletConfig<WalletConnectBase>;
|
||||
};
|
||||
|
||||
+18
-12
@@ -11,7 +11,11 @@ interface Props {
|
||||
showError?: boolean;
|
||||
}
|
||||
|
||||
function WalletLoadingThumbnail({ children, showError }: Props) {
|
||||
function WalletLoadingThumbnail({
|
||||
children,
|
||||
showError,
|
||||
imageSize = 50,
|
||||
}: Props) {
|
||||
const Theme = useAppTheme();
|
||||
const spinValue = useRef(new Animated.Value(0));
|
||||
|
||||
@@ -38,12 +42,17 @@ function WalletLoadingThumbnail({ children, showError }: Props) {
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Svg width={60} height={60} viewBox="0 0 60 60" style={styles.loader}>
|
||||
<Svg
|
||||
width={imageSize + 10}
|
||||
height={imageSize + 10}
|
||||
viewBox={`0 0 ${imageSize + 10} ${imageSize + 10}`}
|
||||
style={styles.loader}
|
||||
>
|
||||
<AnimatedRect
|
||||
x="2"
|
||||
y="2"
|
||||
width={56}
|
||||
height={56}
|
||||
width={imageSize + 6}
|
||||
height={imageSize + 6}
|
||||
rx={15}
|
||||
stroke={showError ? "transparent" : Theme.colors.linkPrimary}
|
||||
strokeWidth={2}
|
||||
@@ -55,8 +64,12 @@ function WalletLoadingThumbnail({ children, showError }: Props) {
|
||||
{showError && (
|
||||
<View
|
||||
style={[
|
||||
styles.error,
|
||||
{
|
||||
position: "absolute",
|
||||
borderWidth: 2,
|
||||
height: imageSize + 6,
|
||||
width: imageSize + 6,
|
||||
borderRadius: 30 / 3.5,
|
||||
borderColor: Theme.colors.border,
|
||||
},
|
||||
]}
|
||||
@@ -75,13 +88,6 @@ const styles = StyleSheet.create({
|
||||
loader: {
|
||||
position: "absolute",
|
||||
},
|
||||
error: {
|
||||
position: "absolute",
|
||||
borderWidth: 2,
|
||||
height: 56,
|
||||
width: 56,
|
||||
borderRadius: 30 / 3.5,
|
||||
},
|
||||
});
|
||||
|
||||
export default WalletLoadingThumbnail;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { walletIds } from "@thirdweb-dev/wallets";
|
||||
|
||||
export type WalletConnectConfig = {
|
||||
projectId?: string;
|
||||
recommended?: boolean;
|
||||
};
|
||||
|
||||
export const walletConnect = (
|
||||
@@ -24,5 +25,6 @@ export const walletConnect = (
|
||||
connectUI(props) {
|
||||
return <WalletConnectUI {...props} projectId={projectId} />;
|
||||
},
|
||||
recommended: config?.recommended,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,141 @@
|
||||
# @thirdweb-dev/react
|
||||
|
||||
## 3.15.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1550](https://github.com/thirdweb-dev/js/pull/1550) [`3f3c63c0`](https://github.com/thirdweb-dev/js/commit/3f3c63c01e34242ae1f074e62b51787b305c059e) Thanks [@MananTank](https://github.com/MananTank)! - # New ConnectWallet UI and APIs
|
||||
|
||||
This introduces a new UI for the ConnectWallet modal and some new APIs along with it.
|
||||
|
||||
### modalSize
|
||||
|
||||
ConnectWallet modal now has two sizes - "wide" and "compact".
|
||||
|
||||
The default is “wide” for desktop. Note that it's always “compact” on mobile device no matter what you pass in.
|
||||
|
||||
```tsx
|
||||
<ConnectWallet modalSize="wide" />
|
||||
```
|
||||
|
||||
```tsx
|
||||
<ConnectWallet modalSize="compact" />
|
||||
```
|
||||
|
||||
### New default wallets
|
||||
|
||||
- MetaMask
|
||||
- Coinbase Wallet
|
||||
- WalletConnect
|
||||
- Trust Wallet
|
||||
- Rainbow
|
||||
- Zerion Wallet
|
||||
- Phantom
|
||||
|
||||
### Theme Customization
|
||||
|
||||
`ConnectWallet`, `Web3Button` and `ThirdwebProvider`'s `theme` prop now also takes an object which you can use to create a custom theme.
|
||||
|
||||
To do this, there are two new functions `darkTheme()` and `lightTheme()` to create this theme object
|
||||
|
||||
```ts
|
||||
import { darkTheme } from '@thirdweb-dev/react'
|
||||
|
||||
const customDarkTheme = darkTheme({
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
color: {
|
||||
modalBg: 'black',
|
||||
accentText: 'red',
|
||||
// ...etc
|
||||
}
|
||||
})
|
||||
|
||||
<ConnectWallet theme={customDarkTheme} />
|
||||
```
|
||||
|
||||
### Show a wallet as “recommended”
|
||||
|
||||
call the wallet configurator function with `{ recommended: true }`
|
||||
|
||||
recommended wallets are shown at the top of the list
|
||||
|
||||
```tsx
|
||||
<ThirdwebProvider
|
||||
supportedWallets={[
|
||||
metamaskWallet({
|
||||
recommended: true,
|
||||
}),
|
||||
coinbaseWalletWallet({
|
||||
recommended: true,
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<App />
|
||||
</ThirdwebProvider>
|
||||
```
|
||||
|
||||
### Show Terms of Service and/or Privacy policy links
|
||||
|
||||
```tsx
|
||||
<ConnectWallet
|
||||
termsOfServiceUrl="https://...."
|
||||
privacyPolicyUrl="https://...."
|
||||
/>
|
||||
```
|
||||
|
||||
### Customize the “Welcome Screen” or Render your own
|
||||
|
||||
The "wide" ConnectWallet modal renders a default welcome screen on the right side which you can customize the texts and image of or completely replace with your own component.
|
||||
|
||||
```tsx
|
||||
// Customize texts and logo
|
||||
|
||||
<ConnectWallet
|
||||
welcomeScreen={{
|
||||
title: "YOUR TITLE",
|
||||
subtitle: "YOUR SUBTITLE",
|
||||
img: {
|
||||
src: "https://...",
|
||||
width: 300,
|
||||
height: 50,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
// or render your own component
|
||||
|
||||
<ConnectWallet
|
||||
welcomeScreen={() => {
|
||||
return <div> ... </div>
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### ConnectWallet’s props are exposed to Web3Button as well
|
||||
|
||||
`Web3Button`` renders a `ConnectWallet``component when the user is not connected, now you can customize that`ConnectWallet`by passing props for it via`connectWallet` object
|
||||
|
||||
```tsx
|
||||
<Web3Button
|
||||
connectWallet={{
|
||||
modalTitle: "Login",
|
||||
modalSize: "compact", // etc..
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1615](https://github.com/thirdweb-dev/js/pull/1615) [`f5991387`](https://github.com/thirdweb-dev/js/commit/f5991387efad368a268f856f56b8a407340807f4) Thanks [@MananTank](https://github.com/MananTank)! - Code cleanup
|
||||
|
||||
- [#1621](https://github.com/thirdweb-dev/js/pull/1621) [`bab39980`](https://github.com/thirdweb-dev/js/commit/bab39980e777be7ec307c2a592bb7966bad1623a) Thanks [@MananTank](https://github.com/MananTank)! - ConnectWallet minor UI fixes
|
||||
|
||||
- Updated dependencies [[`3fd39cea`](https://github.com/thirdweb-dev/js/commit/3fd39cea0df71f80255106329db62660f2fd6e3a), [`3f3c63c0`](https://github.com/thirdweb-dev/js/commit/3f3c63c01e34242ae1f074e62b51787b305c059e), [`48295c06`](https://github.com/thirdweb-dev/js/commit/48295c060499371035980d08e362d9858d0fc18b), [`48295c06`](https://github.com/thirdweb-dev/js/commit/48295c060499371035980d08e362d9858d0fc18b)]:
|
||||
- @thirdweb-dev/[email protected]
|
||||
- @thirdweb-dev/[email protected]
|
||||
- @thirdweb-dev/[email protected]
|
||||
- @thirdweb-dev/[email protected]
|
||||
|
||||
## 3.14.41
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@thirdweb-dev/react",
|
||||
"version": "3.14.41",
|
||||
"version": "3.15.0",
|
||||
"repository": "https://github.com/thirdweb-dev/js/tree/main/packages/react",
|
||||
"author": "thirdweb eng <[email protected]>",
|
||||
"license": "Apache-2.0",
|
||||
@@ -99,6 +99,7 @@
|
||||
"@thirdweb-dev/sdk": "workspace:*",
|
||||
"@thirdweb-dev/tsconfig": "workspace:*",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/qrcode": "^1.5.2",
|
||||
"@types/react": "^18.2.17",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/uuid": "^9.0.2",
|
||||
@@ -139,7 +140,7 @@
|
||||
"copy-to-clipboard": "^3.3.2",
|
||||
"detect-browser": "^5.3.0",
|
||||
"fuse.js": "^6.6.2",
|
||||
"react-qr-code": "^2.0.11",
|
||||
"qrcode": "^1.5.3",
|
||||
"tiny-invariant": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -60,5 +60,5 @@ const ActiveDot = styled.div<{ theme?: Theme }>`
|
||||
top: 60%;
|
||||
right: 0px;
|
||||
background-color: #00d395;
|
||||
box-shadow: 0 0 0 2px ${(p) => p.theme.bg.elevated};
|
||||
box-shadow: 0 0 0 2px var(--bg);
|
||||
`;
|
||||
|
||||
@@ -29,5 +29,5 @@ export const CopyIcon: React.FC<{
|
||||
};
|
||||
|
||||
const CheckIconStyled = /* @__PURE__ */ styled(CheckIcon)<{ theme?: Theme }>`
|
||||
color: ${(p) => p.theme.icon.success};
|
||||
color: ${(p) => p.theme.colors.success};
|
||||
`;
|
||||
|
||||
@@ -91,9 +91,17 @@ export const DragNDrop: React.FC<{
|
||||
{!uploaded ? (
|
||||
<>
|
||||
{" "}
|
||||
<UploadIconSecondary width={iconSize.xl} height={iconSize.xl} />
|
||||
<UploadIconSecondary width={iconSize.lg} height={iconSize.lg} />
|
||||
<Spacer y="md" />
|
||||
<Message>{message}</Message>
|
||||
<Message
|
||||
style={{
|
||||
textAlign: "center",
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
{message}
|
||||
</Message>
|
||||
<Spacer y="md" />
|
||||
{error ? (
|
||||
<ErrorMessage>
|
||||
@@ -120,14 +128,14 @@ export const DragNDrop: React.FC<{
|
||||
const UploadIconSecondary = /* @__PURE__ */ styled(UploadIcon)<{
|
||||
theme?: Theme;
|
||||
}>`
|
||||
color: ${(props) => props.theme.text.secondary};
|
||||
color: ${(props) => props.theme.colors.secondaryIconColor};
|
||||
transition:
|
||||
transform 200ms ease,
|
||||
color 200ms ease;
|
||||
`;
|
||||
|
||||
const DropContainer = styled.div<{ theme?: Theme }>`
|
||||
border: 2px solid ${(p) => p.theme.bg.elevated};
|
||||
border: 2px solid ${(p) => p.theme.colors.borderColor};
|
||||
border-radius: ${radius.md};
|
||||
padding: ${spacing.xl} ${spacing.md};
|
||||
display: flex;
|
||||
@@ -138,25 +146,25 @@ const DropContainer = styled.div<{ theme?: Theme }>`
|
||||
|
||||
&:hover,
|
||||
&[data-is-dragging="true"] {
|
||||
border-color: ${(p) => p.theme.link.primary};
|
||||
border-color: ${(p) => p.theme.colors.accentText};
|
||||
svg {
|
||||
color: ${(p) => p.theme.link.primary};
|
||||
color: ${(p) => p.theme.colors.accentText};
|
||||
}
|
||||
}
|
||||
|
||||
&[data-error="true"] {
|
||||
border-color: ${(p) => p.theme.input.errorRing};
|
||||
border-color: ${(p) => p.theme.colors.danger};
|
||||
}
|
||||
`;
|
||||
|
||||
const ErrorMessage = styled.p<{ theme?: Theme }>`
|
||||
color: ${(p) => p.theme.input.errorRing};
|
||||
color: ${(p) => p.theme.colors.danger};
|
||||
font-size: ${fontSize.sm};
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const ExtensionText = styled.span<{ theme?: Theme }>`
|
||||
color: ${(p) => p.theme.text.secondary};
|
||||
color: ${(p) => p.theme.colors.secondaryText};
|
||||
font-size: ${fontSize.sm};
|
||||
`;
|
||||
|
||||
@@ -186,7 +194,7 @@ const CheckCircleIcon: IconFC = (props) => (
|
||||
);
|
||||
|
||||
const Message = styled.p<{ theme?: Theme }>`
|
||||
color: ${(p) => p.theme.text.neutral};
|
||||
color: ${(p) => p.theme.colors.primaryText};
|
||||
font-size: ${fontSize.md};
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
|
||||
export function DynamicHeight(props: {
|
||||
children: React.ReactNode;
|
||||
maxHeight?: string;
|
||||
}) {
|
||||
const { height, elementRef } = useHeightObserver();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: height ? `${height}px` : "auto",
|
||||
transition: "height 210ms cubic-bezier(0.175, 0.885, 0.32, 1.1)",
|
||||
overflow: "hidden",
|
||||
boxSizing: "border-box",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={elementRef}
|
||||
style={{
|
||||
maxHeight: props.maxHeight,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useHeightObserver() {
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const [height, setHeight] = useState<number | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const element = elementRef.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
setHeight(element.scrollHeight);
|
||||
});
|
||||
|
||||
resizeObserver.observe(element);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [elementRef]);
|
||||
|
||||
return { height, elementRef: elementRef };
|
||||
}
|
||||
@@ -1,14 +1,5 @@
|
||||
import { keyframes } from "@emotion/react";
|
||||
import styled from "@emotion/styled";
|
||||
|
||||
export const fadeInAnimation = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
import { fadeInAnimation } from "../design-system/animations";
|
||||
|
||||
export const FadeIn = styled.div`
|
||||
animation: ${fadeInAnimation} 0.15s ease-in;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useStorage } from "@thirdweb-dev/react-core";
|
||||
|
||||
export const Img: React.FC<{
|
||||
width: string;
|
||||
height: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
src: string;
|
||||
alt?: string;
|
||||
loading?: "eager" | "lazy";
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}> = (props) => {
|
||||
const storage = useStorage();
|
||||
return (
|
||||
@@ -22,9 +23,11 @@ export const Img: React.FC<{
|
||||
loading={props.loading}
|
||||
decoding="async"
|
||||
style={{
|
||||
height: props.height + "px",
|
||||
width: props.width + "px",
|
||||
height: props.height ? props.height + "px" : undefined,
|
||||
width: props.width ? props.width + "px" : undefined,
|
||||
userSelect: "none",
|
||||
}}
|
||||
draggable={false}
|
||||
className={props.className}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -5,24 +5,30 @@ import {
|
||||
shadow,
|
||||
radius,
|
||||
iconSize,
|
||||
fontSize,
|
||||
} from "../design-system";
|
||||
import { scrollbar } from "../design-system/styles";
|
||||
import {
|
||||
widemodalMaxHeight,
|
||||
modalMaxWidthCompact,
|
||||
modalMaxWidthWide,
|
||||
compactmodalMaxHeight,
|
||||
} from "../wallet/ConnectWallet/constants";
|
||||
import { Overlay } from "./Overlay";
|
||||
import { noScrollBar } from "./basic";
|
||||
import { IconButton } from "./buttons";
|
||||
import { keyframes } from "@emotion/react";
|
||||
import styled from "@emotion/styled";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import { DynamicHeight } from "./DynamicHeight";
|
||||
|
||||
export const Modal: React.FC<{
|
||||
trigger?: React.ReactNode;
|
||||
open?: boolean;
|
||||
setOpen?: (open: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
style?: React.CSSProperties;
|
||||
hideCloseIcon?: boolean;
|
||||
size: "wide" | "compact";
|
||||
}> = (props) => {
|
||||
return (
|
||||
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
|
||||
@@ -38,20 +44,28 @@ export const Modal: React.FC<{
|
||||
<Overlay />
|
||||
</Dialog.Overlay>
|
||||
<Dialog.Content asChild>
|
||||
<DialogContent style={props.style}>
|
||||
{props.title && <DialogTitle> {props.title}</DialogTitle>}
|
||||
|
||||
{props.children}
|
||||
<DialogContent
|
||||
style={{
|
||||
height: props.size === "compact" ? "auto" : widemodalMaxHeight,
|
||||
maxWidth:
|
||||
props.size === "compact"
|
||||
? modalMaxWidthCompact
|
||||
: modalMaxWidthWide,
|
||||
}}
|
||||
>
|
||||
{props.size === "compact" ? (
|
||||
<DynamicHeight maxHeight={compactmodalMaxHeight}>
|
||||
{props.children}{" "}
|
||||
</DynamicHeight>
|
||||
) : (
|
||||
props.children
|
||||
)}
|
||||
|
||||
{/* Close Icon */}
|
||||
{!props.hideCloseIcon && (
|
||||
<CrossContainer>
|
||||
<Dialog.Close asChild>
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
type="button"
|
||||
aria-label="Close"
|
||||
>
|
||||
<IconButton type="button" aria-label="Close">
|
||||
<Cross2Icon
|
||||
style={{
|
||||
width: iconSize.md,
|
||||
@@ -70,7 +84,7 @@ export const Modal: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const CrossContainer = styled.div`
|
||||
export const CrossContainer = styled.div`
|
||||
position: absolute;
|
||||
top: ${spacing.lg};
|
||||
right: ${spacing.lg};
|
||||
@@ -104,7 +118,9 @@ const modalAnimationMobile = keyframes`
|
||||
|
||||
const DialogContent = styled.div<{ theme?: Theme }>`
|
||||
z-index: 10000;
|
||||
background-color: ${(p) => p.theme.bg.base};
|
||||
background: ${(p) => p.theme.colors.modalBg};
|
||||
--bg: ${(p) => p.theme.colors.modalBg};
|
||||
color: ${(p) => p.theme.colors.primaryText};
|
||||
border-radius: ${radius.xl};
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
@@ -112,23 +128,15 @@ const DialogContent = styled.div<{ theme?: Theme }>`
|
||||
transform: translate(-50%, -50%);
|
||||
width: calc(100vw - 40px);
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
padding: ${spacing.lg};
|
||||
padding-bottom: ${spacing.xl};
|
||||
animation: ${modalAnimationDesktop} 200ms ease;
|
||||
animation: ${modalAnimationDesktop} 300ms ease;
|
||||
box-shadow: ${shadow.lg};
|
||||
line-height: 1;
|
||||
border: 1px solid ${(p) => p.theme.colors.borderColor};
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
font-family: ${(p) => p.theme.fontFamily};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
${(p) =>
|
||||
scrollbar({
|
||||
track: "transparent",
|
||||
thumb: p.theme.bg.elevated,
|
||||
hover: p.theme.bg.highlighted,
|
||||
})}
|
||||
${noScrollBar}
|
||||
|
||||
/* open from bottom on mobile */
|
||||
${media.mobile} {
|
||||
@@ -147,16 +155,7 @@ const DialogContent = styled.div<{ theme?: Theme }>`
|
||||
}
|
||||
|
||||
& *::selection {
|
||||
background-color: ${(p) => p.theme.bg.inverted};
|
||||
color: ${(p) => p.theme.text.inverted};
|
||||
background-color: ${(p) => p.theme.colors.selectedTextBg};
|
||||
color: ${(p) => p.theme.colors.selectedTextColor};
|
||||
}
|
||||
`;
|
||||
|
||||
const DialogTitle = /* @__PURE__ */ styled.h2<{
|
||||
theme?: Theme;
|
||||
}>`
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
color: ${(p) => p.theme.text.neutral};
|
||||
font-size: ${fontSize.lg};
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Input } from "./formElements";
|
||||
import { Container } from "./basic";
|
||||
import { media, fontSize, spacing } from "../design-system";
|
||||
import styled from "@emotion/styled";
|
||||
|
||||
export function OTPInput(props: {
|
||||
digits: number;
|
||||
isInvalid?: boolean;
|
||||
value: string;
|
||||
setValue: (value: string) => void;
|
||||
onEnter?: () => void;
|
||||
}) {
|
||||
const otp = props.value.split("").map(Number);
|
||||
|
||||
const setOTP = (newOTP: number[]) => {
|
||||
props.setValue(newOTP.join(""));
|
||||
};
|
||||
|
||||
const inputToFocusIndex = otp.length;
|
||||
const boxEls = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (boxEls.current[inputToFocusIndex]) {
|
||||
requestAnimationFrame(() => {
|
||||
boxEls.current[inputToFocusIndex]?.focus();
|
||||
});
|
||||
}
|
||||
}, [inputToFocusIndex]);
|
||||
|
||||
return (
|
||||
<Container center="x" gap="sm" flex="row">
|
||||
{new Array(props.digits).fill(null).map((_, i) => {
|
||||
return (
|
||||
<OTPInputBox
|
||||
data-error={props.isInvalid}
|
||||
ref={(e) => (boxEls.current[i] = e)}
|
||||
key={i}
|
||||
value={otp[i] === undefined ? "" : otp[i]}
|
||||
type="number"
|
||||
variant="outline"
|
||||
onPaste={(e) => {
|
||||
const pastedData = e.clipboardData.getData("text/plain");
|
||||
const newOTP = pastedData
|
||||
.slice(0, props.digits)
|
||||
.split("")
|
||||
.filter((n) => /[0-9]/.test(n))
|
||||
.map(Number);
|
||||
|
||||
setOTP(newOTP);
|
||||
e.preventDefault();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
if (props.onEnter) {
|
||||
props.onEnter();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === "ArrowLeft") {
|
||||
if (i === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
boxEls.current[i - 1]?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowRight") {
|
||||
if (i === props.digits - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
boxEls.current[i + 1]?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === "e" || e.key === ".") {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === "Backspace") {
|
||||
if (i === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newOTP = otp.slice(0, -1);
|
||||
setOTP(newOTP);
|
||||
}
|
||||
}}
|
||||
onChange={(e) => {
|
||||
let value = e.target.value;
|
||||
|
||||
if (value.length > 1) {
|
||||
value = value[value.length - 1];
|
||||
}
|
||||
|
||||
if (!/[0-9]/.test(value) && value !== "") {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const newOTP = [...otp];
|
||||
const index = i > inputToFocusIndex - 1 ? inputToFocusIndex : i;
|
||||
|
||||
newOTP[index] = Number(value);
|
||||
setOTP(newOTP);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const OTPInputBox = /* @__PURE__ */ styled(Input)`
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
font-size: ${fontSize.md};
|
||||
padding: ${spacing.xs};
|
||||
${media.mobile} {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
&[data-verify-status="invalid"] {
|
||||
color: ${(p) => p.theme.colors.danger};
|
||||
border-color: ${(p) => p.theme.colors.danger};
|
||||
}
|
||||
`;
|
||||
@@ -12,9 +12,10 @@ const overlayEnter = keyframes`
|
||||
`;
|
||||
|
||||
export const Overlay = styled.div<{ theme?: Theme }>`
|
||||
background-color: ${(p) => p.theme.overlay.subdued};
|
||||
background-color: ${(p) => p.theme.colors.modalOverlayBg};
|
||||
z-index: 9999;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
animation: ${overlayEnter} 400ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
backdrop-filter: blur(10px);
|
||||
`;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { keyframes } from "@emotion/react";
|
||||
import styled from "@emotion/styled";
|
||||
import * as RXPopover from "@radix-ui/react-popover";
|
||||
import { Container } from "./basic";
|
||||
|
||||
export type PopoverProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -22,7 +23,18 @@ export const Popover = (props: PopoverProps) => {
|
||||
<RXPopover.Trigger asChild>{props.children}</RXPopover.Trigger>
|
||||
<RXPopover.Portal>
|
||||
<PopoverContent sideOffset={7} side="top">
|
||||
<FlexWrapper>{props.content}</FlexWrapper>
|
||||
<Container
|
||||
flex="row"
|
||||
center="y"
|
||||
gap="sm"
|
||||
style={{
|
||||
lineHeight: 1.5,
|
||||
maxWidth: "200px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{props.content}
|
||||
</Container>
|
||||
<PopoverArrow />
|
||||
</PopoverContent>
|
||||
</RXPopover.Portal>
|
||||
@@ -46,24 +58,16 @@ const PopoverContent = /* @__PURE__ */ styled(
|
||||
)<{ theme?: Theme }>`
|
||||
border-radius: ${radius.sm};
|
||||
padding: ${spacing.sm} ${spacing.md};
|
||||
background-color: ${(p) => p.theme.bg.inverted};
|
||||
background-color: ${(p) => p.theme.colors.tooltipBg};
|
||||
box-shadow: ${shadow.md};
|
||||
animation-duration: 400ms;
|
||||
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
will-change: transform, opacity;
|
||||
animation-name: ${slideUpAndFade};
|
||||
color: ${(p) => p.theme.text.inverted};
|
||||
animation: ${slideUpAndFade} 400ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
color: ${(p) => p.theme.colors.tooltipText};
|
||||
font-size: ${fontSize.md};
|
||||
`;
|
||||
|
||||
const PopoverArrow = /* @__PURE__ */ styled(/* @__PURE__ */ RXPopover.Arrow)<{
|
||||
theme?: Theme;
|
||||
}>`
|
||||
fill: ${(p) => p.theme.bg.inverted};
|
||||
`;
|
||||
|
||||
const FlexWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${spacing.sm};
|
||||
fill: ${(p) => p.theme.colors.tooltipBg};
|
||||
`;
|
||||
|
||||
@@ -1,78 +1,305 @@
|
||||
import { media, radius, shadow, spacing } from "../design-system";
|
||||
import { FadeIn } from "./FadeIn";
|
||||
import { Skeleton } from "./Skeleton";
|
||||
import { keyframes } from "@emotion/react";
|
||||
import { Theme, radius } from "../design-system";
|
||||
import styled from "@emotion/styled";
|
||||
import ReactQrCode from "react-qr-code";
|
||||
import QRCodeUtil from "qrcode";
|
||||
import React, { ReactElement, useMemo } from "react";
|
||||
import { fadeInAnimation } from "../design-system/animations";
|
||||
|
||||
export const QRCode: React.FC<{
|
||||
qrCodeUri?: string;
|
||||
QRIcon?: React.ReactNode;
|
||||
size?: number;
|
||||
}> = (props) => {
|
||||
const size = props.size || 280;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{props.qrCodeUri ? (
|
||||
<FadeIn>
|
||||
<QRCodeContainer>
|
||||
<StyledReactQrCode value={props.qrCodeUri} />
|
||||
</QRCodeContainer>
|
||||
</FadeIn>
|
||||
<QRCodeContainer>
|
||||
<QRCodeRenderer
|
||||
uri={props.qrCodeUri}
|
||||
size={size + 20}
|
||||
ecl="M"
|
||||
clearSize={70}
|
||||
/>
|
||||
</QRCodeContainer>
|
||||
) : (
|
||||
<QRSkeleton height="200px" width="200px" />
|
||||
)}
|
||||
{props.QRIcon && (
|
||||
<QrCodeIconContainer>{props.QRIcon}</QrCodeIconContainer>
|
||||
<QRPlaceholder
|
||||
style={{
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
}}
|
||||
>
|
||||
<span data-v1 />
|
||||
<span data-v2 />
|
||||
<span data-v3 />
|
||||
<div />
|
||||
</QRPlaceholder>
|
||||
)}
|
||||
|
||||
{props.QRIcon && <IconContainer>{props.QRIcon}</IconContainer>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const QrCodeIconContainer = styled.div`
|
||||
const IconContainer = styled.div`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
border-radius: ${radius.md};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
padding: 4px;
|
||||
z-index: 1000;
|
||||
`;
|
||||
|
||||
const QRCodeContainer = styled.div`
|
||||
background: white;
|
||||
padding: ${spacing.xxs};
|
||||
const QRCodeContainer = styled.div<{ theme?: Theme }>`
|
||||
animation: ${fadeInAnimation} 600ms ease;
|
||||
--ck-qr-dot-color: ${(p) => p.theme.colors.primaryText};
|
||||
--ck-body-background: ${(p) => p.theme.colors.modalBg};
|
||||
--ck-qr-background: ${(p) => p.theme.colors.modalBg};
|
||||
`;
|
||||
|
||||
const generateMatrix = (
|
||||
value: string,
|
||||
errorCorrectionLevel: QRCodeUtil.QRCodeErrorCorrectionLevel,
|
||||
) => {
|
||||
const arr = Array.prototype.slice.call(
|
||||
QRCodeUtil.create(value, { errorCorrectionLevel }).modules.data,
|
||||
0,
|
||||
);
|
||||
const sqrt = Math.sqrt(arr.length);
|
||||
return arr.reduce(
|
||||
(rows, key, index) =>
|
||||
(index % sqrt === 0
|
||||
? rows.push([key])
|
||||
: rows[rows.length - 1].push(key)) && rows,
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
ecl?: QRCodeUtil.QRCodeErrorCorrectionLevel;
|
||||
size?: number;
|
||||
uri: string;
|
||||
clearSize?: number;
|
||||
image?: React.ReactNode;
|
||||
imageBackground?: string;
|
||||
};
|
||||
|
||||
export const PlaceholderKeyframes = keyframes`
|
||||
0%{ background-position: 100% 0; }
|
||||
100%{ background-position: -100% 0; }
|
||||
`;
|
||||
|
||||
export const QRPlaceholder = styled.div<{ theme?: Theme }>`
|
||||
--color: ${(p) => p.theme.colors.skeletonBg};
|
||||
--bg: ${(p) => p.theme.colors.modalBg};
|
||||
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
border-radius: ${radius.md};
|
||||
box-shadow: ${shadow.md};
|
||||
`;
|
||||
|
||||
const StyledReactQrCode = /* @__PURE__ */ styled(ReactQrCode)`
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: ${radius.md};
|
||||
|
||||
${media.mobile} {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
> div {
|
||||
z-index: 4;
|
||||
position: relative;
|
||||
width: 28%;
|
||||
height: 28%;
|
||||
border-radius: 5px;
|
||||
background: var(--bg);
|
||||
box-shadow: 0 0 0 7px var(--bg);
|
||||
}
|
||||
|
||||
> span {
|
||||
z-index: 4;
|
||||
position: absolute;
|
||||
background: var(--color);
|
||||
border-radius: 12px;
|
||||
width: 13.25%;
|
||||
height: 13.25%;
|
||||
box-shadow: 0 0 0 4px var(--bg);
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 9px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 0 4px var(--bg);
|
||||
}
|
||||
&[data-v1] {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
&[data-v2] {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
&[data-v3] {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
z-index: 3;
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: repeat;
|
||||
background-size: 1.888% 1.888%;
|
||||
background-image: radial-gradient(var(--color) 41%, transparent 41%);
|
||||
}
|
||||
|
||||
&:after {
|
||||
z-index: 100;
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform: scale(1.5) rotate(45deg);
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 50%,
|
||||
${(p) => p.theme.colors.skeletonBg},
|
||||
transparent
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: ${PlaceholderKeyframes} 1000ms linear infinite both;
|
||||
}
|
||||
`;
|
||||
|
||||
const QRSkeleton = /* @__PURE__ */ styled(Skeleton)`
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border-radius: ${radius.md};
|
||||
${media.mobile} {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
`;
|
||||
export function QRCodeRenderer({
|
||||
ecl = "M",
|
||||
size: sizeProp = 200,
|
||||
uri,
|
||||
clearSize = 0,
|
||||
image,
|
||||
imageBackground = "transparent",
|
||||
}: Props) {
|
||||
const logoSize = clearSize;
|
||||
const size = sizeProp - 10 * 2;
|
||||
|
||||
const dots = useMemo(() => {
|
||||
const dotsArray: ReactElement[] = [];
|
||||
const matrix = generateMatrix(uri, ecl);
|
||||
const cellSize = size / matrix.length;
|
||||
const qrList = [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 0 },
|
||||
{ x: 0, y: 1 },
|
||||
];
|
||||
|
||||
qrList.forEach(({ x, y }) => {
|
||||
const x1 = (matrix.length - 7) * cellSize * x;
|
||||
const y1 = (matrix.length - 7) * cellSize * y;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
dotsArray.push(
|
||||
<rect
|
||||
key={`${i}-${x}-${y}`}
|
||||
fill={
|
||||
i % 2 !== 0
|
||||
? "var(--ck-qr-background, var(--ck-body-background))"
|
||||
: "var(--ck-qr-dot-color)"
|
||||
}
|
||||
rx={(i - 2) * -5 + (i === 0 ? 2 : 3)}
|
||||
ry={(i - 2) * -5 + (i === 0 ? 2 : 3)}
|
||||
width={cellSize * (7 - i * 2)}
|
||||
height={cellSize * (7 - i * 2)}
|
||||
x={x1 + cellSize * i}
|
||||
y={y1 + cellSize * i}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (image) {
|
||||
const x1 = (matrix.length - 7) * cellSize * 1;
|
||||
const y1 = (matrix.length - 7) * cellSize * 1;
|
||||
dotsArray.push(
|
||||
<>
|
||||
<rect
|
||||
fill={imageBackground}
|
||||
rx={(0 - 2) * -5 + 2}
|
||||
ry={(0 - 2) * -5 + 2}
|
||||
width={cellSize * (7 - 0 * 2)}
|
||||
height={cellSize * (7 - 0 * 2)}
|
||||
x={x1 + cellSize * 0}
|
||||
y={y1 + cellSize * 0}
|
||||
/>
|
||||
<foreignObject
|
||||
width={cellSize * (7 - 0 * 2)}
|
||||
height={cellSize * (7 - 0 * 2)}
|
||||
x={x1 + cellSize * 0}
|
||||
y={y1 + cellSize * 0}
|
||||
>
|
||||
<div style={{ borderRadius: (0 - 2) * -5 + 2, overflow: "hidden" }}>
|
||||
{image}
|
||||
</div>
|
||||
</foreignObject>
|
||||
</>,
|
||||
);
|
||||
}
|
||||
|
||||
const clearArenaSize = Math.floor((logoSize + 25) / cellSize);
|
||||
const matrixMiddleStart = matrix.length / 2 - clearArenaSize / 2;
|
||||
const matrixMiddleEnd = matrix.length / 2 + clearArenaSize / 2 - 1;
|
||||
|
||||
matrix.forEach((row: QRCodeUtil.QRCode[], i: number) => {
|
||||
row.forEach((_: any, j: number) => {
|
||||
if (matrix[i][j]) {
|
||||
// Do not render dots under position squares
|
||||
if (
|
||||
!(
|
||||
(i < 7 && j < 7) ||
|
||||
(i > matrix.length - 8 && j < 7) ||
|
||||
(i < 7 && j > matrix.length - 8)
|
||||
)
|
||||
) {
|
||||
//if (image && i > matrix.length - 9 && j > matrix.length - 9) return;
|
||||
if (
|
||||
image ||
|
||||
!(
|
||||
i > matrixMiddleStart &&
|
||||
i < matrixMiddleEnd &&
|
||||
j > matrixMiddleStart &&
|
||||
j < matrixMiddleEnd
|
||||
)
|
||||
) {
|
||||
dotsArray.push(
|
||||
<circle
|
||||
key={`circle-${i}-${j}`}
|
||||
cx={i * cellSize + cellSize / 2}
|
||||
cy={j * cellSize + cellSize / 2}
|
||||
fill="var(--ck-qr-dot-color)"
|
||||
r={cellSize / 3}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return dotsArray;
|
||||
}, [ecl, image, imageBackground, logoSize, size, uri]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
height={size}
|
||||
width={size}
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
}}
|
||||
>
|
||||
<rect fill="transparent" height={size} width={size} />
|
||||
{dots}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,16 +2,12 @@ import { radius, Theme } from "../design-system";
|
||||
import { keyframes } from "@emotion/react";
|
||||
import styled from "@emotion/styled";
|
||||
|
||||
type Gradient = { c1: keyof Theme["bg"]; c2: keyof Theme["bg"] };
|
||||
|
||||
export const Skeleton: React.FC<{
|
||||
height: string;
|
||||
width?: string;
|
||||
gradient?: Gradient;
|
||||
}> = (props) => {
|
||||
return (
|
||||
<SkeletonDiv
|
||||
gradient={props.gradient}
|
||||
style={{
|
||||
height: props.height,
|
||||
width: props.width || "auto",
|
||||
@@ -22,17 +18,16 @@ export const Skeleton: React.FC<{
|
||||
|
||||
const skeletonAnimation = keyframes`
|
||||
0% {
|
||||
background-color: var(--skeleton-c-1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
background-color: var(--skeleton-c-2);
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const SkeletonDiv = styled.div<{ theme?: Theme; gradient?: Gradient }>`
|
||||
const SkeletonDiv = styled.div<{ theme?: Theme }>`
|
||||
background-size: 200% 200%;
|
||||
--skeleton-c-1: ${(p) => p.theme.bg[p.gradient?.c1 || "elevated"]};
|
||||
--skeleton-c-2: ${(p) => p.theme.bg[p.gradient?.c2 || "highlighted"]};
|
||||
background-color: ${(p) => p.theme.colors.skeletonBg};
|
||||
animation: ${skeletonAnimation} 500ms ease-in-out infinite alternate;
|
||||
border-radius: ${radius.sm};
|
||||
`;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { keyframes, useTheme } from "@emotion/react";
|
||||
import styled from "@emotion/styled";
|
||||
|
||||
export const Spinner: React.FC<{
|
||||
color: keyof Theme["icon"];
|
||||
color: keyof Theme["colors"];
|
||||
size: keyof typeof iconSize;
|
||||
}> = (props) => {
|
||||
const theme = useTheme() as Theme;
|
||||
@@ -20,7 +20,7 @@ export const Spinner: React.FC<{
|
||||
cy="25"
|
||||
r="20"
|
||||
fill="none"
|
||||
stroke={theme.icon[props.color || "primary"]}
|
||||
stroke={theme.colors[props.color]}
|
||||
strokeWidth="4"
|
||||
/>
|
||||
</Svg>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import styled from "@emotion/styled";
|
||||
import { spacing, Theme } from "../design-system";
|
||||
|
||||
export function Steps({ step }: { step: 1 | 2 }) {
|
||||
return (
|
||||
<StepContainer>
|
||||
<Circle data-active />
|
||||
<Line data-active={step === 2} />
|
||||
<Circle data-active={step === 2} />
|
||||
</StepContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const StepContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 130px;
|
||||
padding: ${spacing.xs};
|
||||
`;
|
||||
|
||||
const Circle = styled.div<{ theme?: Theme }>`
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background-color: ${(p) => p.theme.bg.highlighted};
|
||||
box-shadow:
|
||||
0 0 0 3px ${(p) => p.theme.bg.base},
|
||||
0 0 0 5px ${(p) => p.theme.bg.highlighted};
|
||||
|
||||
&[data-active="true"] {
|
||||
background-color: ${(p) => p.theme.link.primary};
|
||||
box-shadow:
|
||||
0 0 0 3px ${(p) => p.theme.bg.base},
|
||||
0 0 0 5px ${(p) => p.theme.link.primary};
|
||||
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
`;
|
||||
|
||||
const Line = styled.div<{ theme?: Theme }>`
|
||||
flex-grow: 1;
|
||||
height: 4px;
|
||||
background-color: ${(p) => p.theme.bg.highlighted};
|
||||
&[data-active="true"] {
|
||||
background-color: ${(p) => p.theme.link.primary};
|
||||
}
|
||||
`;
|
||||
@@ -4,13 +4,13 @@ import { fontSize, Theme } from "../design-system";
|
||||
export const TextDivider = styled.div<{ theme?: Theme }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${(p) => p.theme.text.secondary};
|
||||
color: ${(p) => p.theme.colors.secondaryText};
|
||||
font-size: ${fontSize.sm};
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
border-bottom: 1px solid ${(p) => p.theme.bg.elevated};
|
||||
border-bottom: 1px solid ${(p) => p.theme.colors.separatorLine};
|
||||
}
|
||||
|
||||
span {
|
||||
|
||||
@@ -45,8 +45,8 @@ const TooltipContent = /* @__PURE__ */ styled(
|
||||
)<{
|
||||
theme?: Theme;
|
||||
}>`
|
||||
background: ${(p) => p.theme.tooltip.bg};
|
||||
color: ${(p) => p.theme.tooltip.text};
|
||||
background: ${(p) => p.theme.colors.tooltipBg};
|
||||
color: ${(p) => p.theme.colors.tooltipText};
|
||||
border-radius: ${radius.sm};
|
||||
line-height: 1;
|
||||
padding: ${spacing.sm} ${spacing.md};
|
||||
@@ -65,5 +65,5 @@ const TooltipArrow = /* @__PURE__ */ styled(
|
||||
)<{
|
||||
theme?: Theme;
|
||||
}>`
|
||||
fill: ${(p) => p.theme.tooltip.bg};
|
||||
fill: ${(p) => p.theme.colors.tooltipBg};
|
||||
`;
|
||||
|
||||
@@ -1,28 +1,184 @@
|
||||
import { spacing } from "../design-system";
|
||||
import styled from "@emotion/styled";
|
||||
import { Theme, iconSize, spacing } from "../design-system";
|
||||
import { BackButton, ModalTitle } from "./modalElements";
|
||||
import { Img } from "./Img";
|
||||
import {
|
||||
fadeInAnimation,
|
||||
floatDownAnimation,
|
||||
floatUpAnimation,
|
||||
} from "../design-system/animations";
|
||||
|
||||
export const Flex = (props: {
|
||||
flexDirection?: "row" | "column";
|
||||
justifyContent?: "flex-start" | "flex-end" | "center" | "space-between";
|
||||
alignItems?: "flex-start" | "flex-end" | "center" | "stretch";
|
||||
wrap?: "wrap" | "nowrap";
|
||||
gap?: keyof typeof spacing;
|
||||
children: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}) => {
|
||||
export const ScreenBottomContainer = styled.div<{ theme?: Theme }>`
|
||||
border-top: 1px solid ${(p) => p.theme.colors.separatorLine};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${spacing.lg};
|
||||
padding: ${spacing.lg};
|
||||
`;
|
||||
|
||||
export const noScrollBar = `
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export function ModalHeader(props: {
|
||||
onBack?: () => void;
|
||||
title: React.ReactNode;
|
||||
imgSrc?: string;
|
||||
}) {
|
||||
const { onBack, title, imgSrc } = props;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: props.flexDirection,
|
||||
justifyContent: props.justifyContent,
|
||||
alignItems: props.alignItems,
|
||||
flexWrap: props.wrap,
|
||||
gap: props.gap ? spacing[props.gap] : undefined,
|
||||
...(props.style || {}),
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
{props.children}
|
||||
{onBack && (
|
||||
<BackButton
|
||||
onClick={onBack}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Container flex="row" gap="xs" center="both">
|
||||
{imgSrc && (
|
||||
<Img src={imgSrc} width={iconSize.md} height={iconSize.md} />
|
||||
)}
|
||||
{typeof title === "string" ? <ModalTitle>{title}</ModalTitle> : title}
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const Line = styled.div<{
|
||||
theme?: Theme;
|
||||
}>`
|
||||
height: 1px;
|
||||
background: ${(p) => p.theme.colors.separatorLine};
|
||||
`;
|
||||
|
||||
export function Container(props: {
|
||||
animate?: "fadein" | "floatup" | "floatdown";
|
||||
fullHeight?: boolean;
|
||||
flex?: "row" | "column";
|
||||
expand?: boolean;
|
||||
center?: "x" | "y" | "both";
|
||||
gap?: keyof typeof spacing;
|
||||
children: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
p?: keyof typeof spacing;
|
||||
px?: keyof typeof spacing;
|
||||
relative?: boolean;
|
||||
scrollY?: boolean;
|
||||
color?: keyof Theme["colors"];
|
||||
debug?: boolean;
|
||||
}) {
|
||||
const styles: React.CSSProperties = {};
|
||||
|
||||
if (props.relative) {
|
||||
styles.position = "relative";
|
||||
}
|
||||
|
||||
if (props.fullHeight) {
|
||||
styles.height = "100%";
|
||||
}
|
||||
|
||||
if (props.expand) {
|
||||
styles.flex = "1 1 0%";
|
||||
}
|
||||
|
||||
if (props.flex) {
|
||||
styles.display = "flex";
|
||||
styles.flexDirection = props.flex;
|
||||
|
||||
if (props.flex === "row") {
|
||||
styles.flexWrap = "wrap";
|
||||
}
|
||||
|
||||
if (props.gap) {
|
||||
styles.gap = spacing[props.gap];
|
||||
}
|
||||
|
||||
if (props.center) {
|
||||
if (props.center === "both") {
|
||||
styles.justifyContent = "center";
|
||||
styles.alignItems = "center";
|
||||
}
|
||||
|
||||
if (
|
||||
(props.center === "x" && props.flex === "row") ||
|
||||
(props.center === "y" && props.flex === "column")
|
||||
) {
|
||||
styles.justifyContent = "center";
|
||||
}
|
||||
|
||||
if (
|
||||
(props.center === "x" && props.flex === "column") ||
|
||||
(props.center === "y" && props.flex === "row")
|
||||
) {
|
||||
styles.alignItems = "center";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (props.p) {
|
||||
styles.padding = spacing[props.p];
|
||||
}
|
||||
|
||||
if (props.px) {
|
||||
styles.paddingLeft = spacing[props.px];
|
||||
styles.paddingRight = spacing[props.px];
|
||||
}
|
||||
|
||||
if (props.debug) {
|
||||
styles.outline = "1px solid red";
|
||||
styles.outlineOffset = "-1px";
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
data-scrolly={props.scrollY}
|
||||
data-animate={props.animate}
|
||||
color={props.color}
|
||||
style={{
|
||||
...styles,
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const Box = styled.div<{ theme?: Theme; color?: keyof Theme["colors"] }>`
|
||||
color: ${(p) => (p.color ? p.theme.colors[p.color] : "inherit")};
|
||||
|
||||
&[data-animate="fadein"] {
|
||||
opacity: 0;
|
||||
animation: ${fadeInAnimation} 350ms ease forwards;
|
||||
}
|
||||
|
||||
&[data-animate="floatup"] {
|
||||
opacity: 0;
|
||||
animation: ${floatUpAnimation} 350ms ease forwards;
|
||||
}
|
||||
|
||||
&[data-animate="floatdown"] {
|
||||
opacity: 0;
|
||||
animation: ${floatDownAnimation} 350ms ease forwards;
|
||||
}
|
||||
|
||||
&[data-scrolly="true"] {
|
||||
overflow-y: auto;
|
||||
${noScrollBar}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,88 +1,75 @@
|
||||
import { fontSize, radius, spacing, Theme } from "../design-system";
|
||||
import styled from "@emotion/styled";
|
||||
|
||||
// for rendering a conventional button
|
||||
export const Button = styled.button<{
|
||||
variant: "inverted" | "secondary" | "link" | "danger";
|
||||
variant: "primary" | "secondary" | "link" | "accent" | "outline";
|
||||
theme?: Theme;
|
||||
fullWidth?: boolean;
|
||||
}>`
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: ${radius.md};
|
||||
padding: ${spacing.sm} ${spacing.sm};
|
||||
padding: ${fontSize.sm} ${fontSize.sm};
|
||||
font-size: ${fontSize.md};
|
||||
font-weight: 500;
|
||||
font-weight: ${(p) => (p.variant === "link" ? 400 : 500)};
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 3px
|
||||
${(p) => {
|
||||
switch (p.variant) {
|
||||
case "inverted":
|
||||
return p.theme.bg.invertedFocused;
|
||||
case "secondary":
|
||||
return p.theme.bg.highlighted;
|
||||
case "link":
|
||||
return "none";
|
||||
case "danger":
|
||||
return p.theme.text.danger;
|
||||
}
|
||||
}};
|
||||
}
|
||||
${(p) => p.fullWidth && `width: 100%;`};
|
||||
|
||||
box-shadow: ${(p) => {
|
||||
background: ${(p) => {
|
||||
switch (p.variant) {
|
||||
case "danger":
|
||||
return `0 0 0 2px ${p.theme.text.danger}`;
|
||||
case "link":
|
||||
return "none";
|
||||
case "primary":
|
||||
return p.theme.colors.primaryButtonBg;
|
||||
case "accent":
|
||||
return p.theme.colors.accentButtonBg;
|
||||
case "secondary":
|
||||
return p.theme.colors.secondaryButtonBg;
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}};
|
||||
|
||||
background: ${(p) => {
|
||||
switch (p.variant) {
|
||||
case "inverted":
|
||||
return p.theme.bg.inverted;
|
||||
case "secondary":
|
||||
return p.theme.bg.elevated;
|
||||
case "link":
|
||||
return "transparent";
|
||||
case "danger":
|
||||
return "none";
|
||||
}
|
||||
}};
|
||||
color: ${(p) => {
|
||||
switch (p.variant) {
|
||||
case "inverted":
|
||||
return p.theme.text.inverted;
|
||||
case "primary":
|
||||
return p.theme.colors.primaryButtonText;
|
||||
case "accent":
|
||||
return p.theme.colors.accentButtonText;
|
||||
case "secondary":
|
||||
return p.theme.text.neutral;
|
||||
return p.theme.colors.secondaryButtonText;
|
||||
case "outline":
|
||||
return p.theme.colors.secondaryButtonText;
|
||||
case "link":
|
||||
return p.theme.link.primary;
|
||||
case "danger":
|
||||
return p.theme.text.danger;
|
||||
return p.theme.colors.accentText;
|
||||
default:
|
||||
return p.theme.colors.primaryButtonText;
|
||||
}
|
||||
}};
|
||||
|
||||
${(p) => {
|
||||
if (p.variant === "outline") {
|
||||
return `
|
||||
border: 1.5px solid ${p.theme.colors.borderColor};
|
||||
`;
|
||||
}
|
||||
}}
|
||||
|
||||
${(p) => {
|
||||
if (p.variant === "link") {
|
||||
return `
|
||||
padding: 0;
|
||||
&:hover {
|
||||
color: ${p.theme.text.neutral};
|
||||
color: ${p.theme.colors.primaryText};
|
||||
}`;
|
||||
}
|
||||
}}
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
/* pressed effect */
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
@@ -93,9 +80,7 @@ export const Button = styled.button<{
|
||||
}
|
||||
`;
|
||||
|
||||
// for rendering a button with an icon
|
||||
export const IconButton = styled.button<{
|
||||
variant: "neutral" | "secondary";
|
||||
theme?: Theme;
|
||||
}>`
|
||||
all: unset;
|
||||
@@ -105,25 +90,18 @@ export const IconButton = styled.button<{
|
||||
justify-content: center;
|
||||
border-radius: ${radius.sm};
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
color: ${(p) => {
|
||||
switch (p.variant) {
|
||||
case "neutral":
|
||||
return p.theme.text.neutral;
|
||||
case "secondary":
|
||||
return p.theme.text.secondary;
|
||||
}
|
||||
}};
|
||||
color: ${(p) => p.theme.colors.secondaryIconColor};
|
||||
padding: 2px;
|
||||
transition:
|
||||
background 0.2s ease,
|
||||
color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(p) => p.theme.bg.elevated};
|
||||
color: ${(p) => p.theme.text.neutral};
|
||||
background: ${(p) => p.theme.colors.secondaryIconHoverBg};
|
||||
color: ${(p) => p.theme.colors.secondaryIconHoverColor};
|
||||
}
|
||||
`;
|
||||
|
||||
// for rendering a button next to input
|
||||
export const InputButton = styled.button<{ theme?: Theme }>`
|
||||
all: unset;
|
||||
display: flex;
|
||||
@@ -133,9 +111,9 @@ export const InputButton = styled.button<{ theme?: Theme }>`
|
||||
padding: ${spacing.sm};
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
color: ${(p) => p.theme.text.secondary};
|
||||
color: ${(p) => p.theme.colors.secondaryText};
|
||||
&:hover {
|
||||
color: ${(p) => p.theme.text.neutral};
|
||||
color: ${(p) => p.theme.colors.primaryText};
|
||||
}
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
|
||||
@@ -3,69 +3,61 @@ import styled from "@emotion/styled";
|
||||
|
||||
export const Label = styled.label<{ theme?: Theme }>`
|
||||
font-size: ${fontSize.sm};
|
||||
color: ${(p) => p.theme.text.neutral};
|
||||
color: ${(p) => p.theme.colors.primaryText};
|
||||
display: block;
|
||||
`;
|
||||
|
||||
export const Input = styled.input<{
|
||||
variant: "outline" | "transparent" | "secondary";
|
||||
variant: "outline" | "transparent";
|
||||
theme?: Theme;
|
||||
}>`
|
||||
font-size: ${fontSize.md};
|
||||
display: block;
|
||||
padding: ${spacing.sm};
|
||||
padding: ${fontSize.sm};
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: ${(p) => p.theme.text.neutral};
|
||||
color: ${(p) => p.theme.colors.primaryText};
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: ${(p) => {
|
||||
switch (p.variant) {
|
||||
case "secondary":
|
||||
return p.theme.bg.elevated;
|
||||
default:
|
||||
return "transparent";
|
||||
}
|
||||
}};
|
||||
background: transparent;
|
||||
|
||||
&::placeholder {
|
||||
color: ${(p) => p.theme.text.secondary};
|
||||
color: ${(p) => p.theme.colors.secondaryText};
|
||||
}
|
||||
|
||||
box-shadow: 0 0 0 1.5px
|
||||
${(p) => {
|
||||
switch (p.variant) {
|
||||
case "outline":
|
||||
return p.theme.input.outline;
|
||||
case "transparent":
|
||||
return p.theme.colors.borderColor;
|
||||
default:
|
||||
return "transparent";
|
||||
case "secondary":
|
||||
return p.theme.bg.elevated;
|
||||
}
|
||||
}};
|
||||
|
||||
/* when browser auto-fills the input */
|
||||
&:-webkit-autofill {
|
||||
-webkit-text-fill-color: ${(p) => p.theme.text.neutral};
|
||||
-webkit-box-shadow: 0 0 0px 1000px ${(p) => p.theme.bg.elevated} inset !important;
|
||||
box-shadow: 0 0 0px 1000px ${(p) => p.theme.bg.elevated} inset !important;
|
||||
-webkit-text-fill-color: ${(p) => p.theme.colors.primaryText};
|
||||
-webkit-box-shadow: 0 0 0px 1000px ${(p) => p.theme.colors.inputAutofillBg}
|
||||
inset !important;
|
||||
box-shadow: 0 0 0px 1000px ${(p) => p.theme.colors.inputAutofillBg} inset !important;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
&:-webkit-autofill:focus {
|
||||
-webkit-box-shadow:
|
||||
0 0 0px 1000px ${(p) => p.theme.bg.elevated} inset,
|
||||
0 0 0 2px ${(p) => p.theme.input.focusRing} !important;
|
||||
0 0 0px 1000px ${(p) => p.theme.colors.inputAutofillBg} inset,
|
||||
0 0 0 2px ${(p) => p.theme.colors.accentText} !important;
|
||||
box-shadow:
|
||||
0 0 0px 1000px ${(p) => p.theme.bg.elevated} inset,
|
||||
0 0 0 2px ${(p) => p.theme.input.focusRing} !important;
|
||||
0 0 0px 1000px ${(p) => p.theme.colors.inputAutofillBg} inset,
|
||||
0 0 0 2px ${(p) => p.theme.colors.accentText} !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px ${(p) => p.theme.input.focusRing};
|
||||
box-shadow: 0 0 0 2px ${(p) => p.theme.colors.accentText};
|
||||
}
|
||||
|
||||
/* show overflow ellipsis for long text - but not if it's a type="password" */
|
||||
@@ -76,23 +68,33 @@ export const Input = styled.input<{
|
||||
}
|
||||
|
||||
&[data-error="true"] {
|
||||
box-shadow: 0 0 0 2px ${(p) => p.theme.input.errorRing} !important;
|
||||
box-shadow: 0 0 0 2px ${(p) => p.theme.colors.danger} !important;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&[type="number"]::-webkit-outer-spin-button,
|
||||
&[type="number"]::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
&[type="number"] {
|
||||
appearance: none;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
`;
|
||||
|
||||
// for rendering a input and a button side by side
|
||||
export const InputContainer = styled.div<{ theme?: Theme }>`
|
||||
display: flex;
|
||||
border-radius: ${radius.sm};
|
||||
box-shadow: 0 0 0px 1.5px ${(p) => p.theme.input.outline};
|
||||
box-shadow: 0 0 0px 1.5px ${(p) => p.theme.colors.borderColor};
|
||||
|
||||
/* show focus ring on container instead of input */
|
||||
&:focus-within {
|
||||
box-shadow: 0 0 0px 2px ${(p) => p.theme.input.focusRing};
|
||||
box-shadow: 0 0 0px 2px ${(p) => p.theme.colors.accentText};
|
||||
}
|
||||
|
||||
input:focus {
|
||||
@@ -101,7 +103,7 @@ export const InputContainer = styled.div<{ theme?: Theme }>`
|
||||
|
||||
/* show error ring on container instead of input */
|
||||
&[data-error="true"] {
|
||||
box-shadow: 0 0 0px 2px ${(p) => p.theme.input.errorRing};
|
||||
box-shadow: 0 0 0px 2px ${(p) => p.theme.colors.danger};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -109,8 +111,8 @@ export const ErrorMessage = styled.p<{ theme?: Theme }>`
|
||||
all: unset;
|
||||
font-size: ${fontSize.sm};
|
||||
display: block;
|
||||
color: ${(p) => p.theme.input.errorRing};
|
||||
line-height: 1.5;
|
||||
color: ${(p) => p.theme.colors.danger};
|
||||
line-height: 1;
|
||||
`;
|
||||
|
||||
export const FormFooter = styled.div`
|
||||
|
||||
@@ -11,7 +11,8 @@ export const ModalTitle = /* @__PURE__ */ styled.h2<{
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: ${fontSize.lg};
|
||||
color: ${(p) => p.theme.text.neutral};
|
||||
color: ${(p) => p.theme.colors.primaryText};
|
||||
line-height: 1.3;
|
||||
text-align: left;
|
||||
${media.mobile} {
|
||||
text-align: ${(p) => (p.centerOnMobile ? "center" : "left")};
|
||||
@@ -26,7 +27,7 @@ export const ModalDescription = styled.p<{
|
||||
all: unset;
|
||||
display: block;
|
||||
font-size: ${(p) => (p.sm ? fontSize.sm : fontSize.md)};
|
||||
color: ${(p) => p.theme.text.secondary};
|
||||
color: ${(p) => p.theme.colors.secondaryText};
|
||||
line-height: 1.5;
|
||||
${media.mobile} {
|
||||
text-align: ${(p) => (p.centerOnMobile ? "center" : "left")};
|
||||
@@ -39,9 +40,8 @@ export const BackButton: React.FC<{
|
||||
}> = (props) => {
|
||||
return (
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
onClick={props.onClick}
|
||||
style={props.style}
|
||||
style={{ transform: "translateX(-25%)", ...props.style }}
|
||||
type="button"
|
||||
>
|
||||
<ChevronLeftIcon
|
||||
@@ -57,15 +57,16 @@ export const BackButton: React.FC<{
|
||||
export const HelperLink = styled.a<{ theme?: Theme; md?: boolean }>`
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
color: ${(p) => p.theme.link.primary};
|
||||
color: ${(p) => p.theme.colors.accentText};
|
||||
font-size: ${(p) => (p.md ? fontSize.md : fontSize.sm)};
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
${media.mobile} {
|
||||
text-align: center;
|
||||
}
|
||||
&:hover {
|
||||
color: ${(p) => p.theme.link.primaryHover};
|
||||
color: ${(p) => p.theme.colors.primaryText};
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,20 +1,47 @@
|
||||
import styled from "@emotion/styled";
|
||||
import { fontSize, Theme } from "../design-system";
|
||||
|
||||
export const SecondaryText = styled.span<{ theme?: Theme }>`
|
||||
font-size: ${fontSize.md};
|
||||
color: ${(p) => p.theme.text.secondary};
|
||||
export const Text = styled.span<{
|
||||
theme?: Theme;
|
||||
color?: keyof Theme["colors"];
|
||||
center?: boolean;
|
||||
inline?: boolean;
|
||||
size?: keyof typeof fontSize;
|
||||
weight?: 400 | 500 | 600 | 700;
|
||||
multiline?: boolean;
|
||||
balance?: boolean;
|
||||
}>`
|
||||
font-size: ${(p) => fontSize[p.size || "md"]};
|
||||
color: ${(p) => p.theme.colors[p.color || "secondaryText"]};
|
||||
margin: 0;
|
||||
display: ${(p) => (p.inline ? "inline" : "block")};
|
||||
font-weight: ${(p) => p.weight || 400};
|
||||
line-height: ${(p) => (p.multiline ? 1.5 : 1)};
|
||||
${(p) => (p.center ? `text-align: center;` : "")};
|
||||
text-wrap: ${(p) => (p.balance ? "balance" : "inherit")};
|
||||
`;
|
||||
|
||||
export const NeutralText = styled.span<{ theme?: Theme }>`
|
||||
font-size: ${fontSize.md};
|
||||
color: ${(p) => p.theme.text.neutral};
|
||||
margin: 0;
|
||||
`;
|
||||
export const Link = styled.a<{
|
||||
theme?: Theme;
|
||||
size?: keyof typeof fontSize;
|
||||
secondary?: boolean;
|
||||
weight?: 400 | 500 | 600 | 700;
|
||||
inline?: boolean;
|
||||
center?: boolean;
|
||||
}>`
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
color: ${(p) =>
|
||||
p.secondary ? p.theme.colors.secondaryText : p.theme.colors.accentText};
|
||||
font-size: ${(p) => fontSize[p.size || "md"]};
|
||||
text-decoration: none;
|
||||
text-align: ${(p) => (p.center ? "center" : "left")};
|
||||
display: ${(p) => (p.inline ? "inline" : "block")};
|
||||
font-weight: ${(p) => p.weight || 400};
|
||||
line-height: 1;
|
||||
|
||||
export const DangerText = styled.span<{ theme?: Theme }>`
|
||||
font-size: ${fontSize.md};
|
||||
color: ${(p) => p.theme.text.danger};
|
||||
margin: 0;
|
||||
&:hover {
|
||||
color: ${(p) => p.theme.colors.primaryText};
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
import { Theme, darkThemeObj, lightThemeObj } from "./index";
|
||||
|
||||
export function CustomThemeProvider(props: {
|
||||
children: React.ReactNode;
|
||||
theme: "light" | "dark" | Theme;
|
||||
}) {
|
||||
const { theme, children } = props;
|
||||
let themeObj: Theme;
|
||||
|
||||
if (typeof theme === "string") {
|
||||
themeObj = theme === "light" ? lightThemeObj : darkThemeObj;
|
||||
} else {
|
||||
themeObj = theme;
|
||||
}
|
||||
|
||||
return <ThemeProvider theme={themeObj}>{children}</ThemeProvider>;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { keyframes } from "@emotion/react";
|
||||
|
||||
export const floatUpAnimation = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20%) scale(0.8) ;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
|
||||
export const floatDownAnimation = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20%) scale(0.8) ;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
|
||||
export const fadeInAnimation = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
@@ -1,110 +1,97 @@
|
||||
import { blue, green, mauve, mauveDark, tomato } from "@radix-ui/colors";
|
||||
import { green, mauve, mauveDark, tomato } from "@radix-ui/colors";
|
||||
|
||||
export const darkTheme = {
|
||||
overlay: {
|
||||
subdued: "rgba(0, 0, 0, 0.74)",
|
||||
},
|
||||
bg: {
|
||||
base: mauveDark.mauve1,
|
||||
baseHover: mauveDark.mauve2,
|
||||
elevated: mauveDark.mauve3,
|
||||
elevatedHover: mauveDark.mauve4,
|
||||
highlighted: mauveDark.mauve5,
|
||||
inverted: mauveDark.mauve12,
|
||||
invertedFocused: mauveDark.mauve7,
|
||||
danger: tomato.tomato12,
|
||||
},
|
||||
border: {
|
||||
base: mauveDark.mauve3,
|
||||
elevated: mauveDark.mauve4,
|
||||
},
|
||||
badge: {
|
||||
secondary: mauveDark.mauve6,
|
||||
},
|
||||
text: {
|
||||
neutral: mauveDark.mauve12,
|
||||
inverted: mauveDark.mauve1,
|
||||
secondary: mauveDark.mauve9,
|
||||
danger: tomato.tomato9,
|
||||
},
|
||||
icon: {
|
||||
secondary: mauveDark.mauve11,
|
||||
primary: mauveDark.mauve12,
|
||||
inverted: mauveDark.mauve1,
|
||||
danger: tomato.tomato9,
|
||||
success: green.green7,
|
||||
link: blue.blue9,
|
||||
},
|
||||
link: {
|
||||
primary: blue.blue9,
|
||||
primaryHover: mauveDark.mauve12,
|
||||
},
|
||||
input: {
|
||||
bg: mauveDark.mauve3,
|
||||
bgHover: mauveDark.mauve5,
|
||||
outline: mauveDark.mauve5,
|
||||
focusRing: blue.blue9,
|
||||
errorRing: tomato.tomato9,
|
||||
},
|
||||
tooltip: {
|
||||
bg: "black",
|
||||
text: "white",
|
||||
},
|
||||
const darkColors = {
|
||||
base1: "hsl(230deg 11.63% 8.43%)",
|
||||
base2: "hsl(230deg 11.63% 12%)",
|
||||
base3: "hsl(230deg 11.63% 15%)",
|
||||
base4: "hsl(230deg 11.63% 17%)",
|
||||
primaryText: mauveDark.mauve12,
|
||||
secondaryText: mauveDark.mauve10,
|
||||
danger: tomato.tomato9,
|
||||
success: green.green7,
|
||||
overlay: "rgba(0, 0, 0, 0.7)",
|
||||
accentText: "#3385FF",
|
||||
accentBg: "hsl(216 100% 50%)",
|
||||
textOnAccent: mauveDark.mauve12,
|
||||
};
|
||||
|
||||
export const lightTheme: typeof darkTheme = {
|
||||
overlay: {
|
||||
subdued: "rgba(0, 0, 0, 0.4)",
|
||||
},
|
||||
bg: {
|
||||
base: mauve.mauve1,
|
||||
baseHover: mauve.mauve2,
|
||||
elevated: mauve.mauve3,
|
||||
elevatedHover: mauve.mauve4,
|
||||
highlighted: mauve.mauve5,
|
||||
inverted: mauve.mauve12,
|
||||
invertedFocused: mauve.mauve7,
|
||||
danger: tomato.tomato4,
|
||||
},
|
||||
border: {
|
||||
base: mauve.mauve6,
|
||||
elevated: mauve.mauve7,
|
||||
},
|
||||
badge: {
|
||||
secondary: mauve.mauve1,
|
||||
},
|
||||
text: {
|
||||
neutral: mauve.mauve12,
|
||||
inverted: mauve.mauve1,
|
||||
secondary: mauve.mauve10,
|
||||
danger: tomato.tomato9,
|
||||
},
|
||||
icon: {
|
||||
secondary: mauve.mauve10,
|
||||
primary: mauve.mauve12,
|
||||
inverted: mauve.mauve1,
|
||||
danger: tomato.tomato9,
|
||||
success: green.green9,
|
||||
link: blue.blue11,
|
||||
},
|
||||
link: {
|
||||
primary: blue.blue11,
|
||||
primaryHover: mauve.mauve12,
|
||||
},
|
||||
input: {
|
||||
bg: mauve.mauve3,
|
||||
bgHover: mauveDark.mauve5,
|
||||
outline: mauve.mauve7,
|
||||
focusRing: blue.blue11,
|
||||
errorRing: tomato.tomato10,
|
||||
},
|
||||
tooltip: {
|
||||
bg: "black",
|
||||
text: "white",
|
||||
},
|
||||
const lightColors: typeof darkColors = {
|
||||
base1: mauve.mauve1,
|
||||
base2: mauve.mauve3,
|
||||
base3: mauve.mauve5,
|
||||
base4: mauve.mauve6,
|
||||
primaryText: mauve.mauve12,
|
||||
secondaryText: mauveDark.mauve9,
|
||||
accentText: "hsl(216 100% 45%)",
|
||||
success: green.green9,
|
||||
danger: tomato.tomato9,
|
||||
overlay: "rgba(0, 0, 0, 0.7)",
|
||||
accentBg: "hsl(216 100% 50%)",
|
||||
textOnAccent: mauve.mauve1,
|
||||
};
|
||||
|
||||
export type Theme = typeof darkTheme;
|
||||
function createThemeObj(colors: typeof darkColors) {
|
||||
return {
|
||||
type: "dark" as "light" | "dark",
|
||||
colors: {
|
||||
primaryText: colors.primaryText,
|
||||
secondaryText: colors.secondaryText,
|
||||
accentText: colors.accentText,
|
||||
|
||||
danger: colors.danger,
|
||||
success: colors.success,
|
||||
|
||||
modalOverlayBg: colors.overlay,
|
||||
|
||||
accentButtonBg: colors.accentBg,
|
||||
accentButtonText: colors.textOnAccent,
|
||||
|
||||
primaryButtonBg: colors.primaryText,
|
||||
primaryButtonText: colors.base1,
|
||||
|
||||
secondaryButtonBg: colors.base3,
|
||||
secondaryButtonText: colors.primaryText,
|
||||
secondaryButtonHoverBg: colors.base4,
|
||||
|
||||
modalBg: colors.base1,
|
||||
dropdownBg: colors.base1,
|
||||
|
||||
tooltipBg: colors.primaryText,
|
||||
tooltipText: colors.base1,
|
||||
|
||||
inputAutofillBg: colors.base2,
|
||||
scrollbarBg: colors.base2,
|
||||
walletSelectorButtonHoverBg: colors.base2,
|
||||
|
||||
separatorLine: colors.base3,
|
||||
|
||||
secondaryIconColor: colors.secondaryText,
|
||||
secondaryIconHoverBg: colors.base3,
|
||||
secondaryIconHoverColor: colors.primaryText,
|
||||
|
||||
borderColor: colors.base3,
|
||||
skeletonBg: colors.base3,
|
||||
|
||||
selectedTextColor: colors.base1,
|
||||
selectedTextBg: colors.primaryText,
|
||||
|
||||
connectedButtonBg: colors.base1,
|
||||
connectedButtonBgHover: colors.base2,
|
||||
},
|
||||
fontFamily: "inherit",
|
||||
};
|
||||
}
|
||||
|
||||
export const darkThemeObj = /* @__PURE__ */ createThemeObj(darkColors);
|
||||
export const lightThemeObj = /* @__PURE__ */ createThemeObj(lightColors);
|
||||
|
||||
export type Theme = typeof darkThemeObj;
|
||||
|
||||
export type ThemeObjectOrType = "light" | "dark" | Theme;
|
||||
|
||||
export type ThemeOverrides = {
|
||||
[key in Exclude<keyof Theme, "type">]?: Partial<Theme[key]>;
|
||||
};
|
||||
|
||||
export const fontSize = {
|
||||
xs: "12px",
|
||||
@@ -138,6 +125,7 @@ export const iconSize = {
|
||||
md: "24",
|
||||
lg: "32",
|
||||
xl: "48",
|
||||
xxl: "64",
|
||||
};
|
||||
|
||||
// desktop first style media query
|
||||
@@ -145,9 +133,35 @@ export const media = {
|
||||
mobile: `@media (max-width: 640px)`,
|
||||
};
|
||||
|
||||
// TODO - move to theme
|
||||
export const shadow = {
|
||||
sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
|
||||
md: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
|
||||
lg: "0 10px 15px -3px rgb(0 0 0 / 0.07), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
|
||||
xl: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)",
|
||||
};
|
||||
|
||||
export function lightTheme(overrides: ThemeOverrides): Theme {
|
||||
return applyThemeOverrides(lightThemeObj, overrides);
|
||||
}
|
||||
|
||||
export function darkTheme(overrides: ThemeOverrides): Theme {
|
||||
return applyThemeOverrides(darkThemeObj, overrides);
|
||||
}
|
||||
|
||||
export function applyThemeOverrides(
|
||||
baseTheme: Theme,
|
||||
themeOverides: ThemeOverrides,
|
||||
): Theme {
|
||||
const theme = { ...baseTheme };
|
||||
|
||||
if (themeOverides.colors) {
|
||||
theme.colors = { ...theme.colors, ...themeOverides.colors };
|
||||
}
|
||||
|
||||
if (themeOverides.fontFamily) {
|
||||
theme.fontFamily = themeOverides.fontFamily;
|
||||
}
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Popover } from "../../../components/Popover";
|
||||
import { Spinner } from "../../../components/Spinner";
|
||||
import { Button } from "../../../components/buttons";
|
||||
import { darkTheme, lightTheme } from "../../../design-system";
|
||||
import { ConnectWallet } from "../../../wallet/ConnectWallet/ConnectWallet";
|
||||
import { Theme, ThemeObjectOrType } from "../../../design-system";
|
||||
import {
|
||||
ConnectWallet,
|
||||
ConnectWalletProps,
|
||||
} from "../../../wallet/ConnectWallet/ConnectWallet";
|
||||
import { useIsHeadlessWallet } from "../../../wallet/hooks/useIsHeadlessWallet";
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
ThirdwebThemeContext,
|
||||
useAddress,
|
||||
useContract,
|
||||
useNetworkMismatch,
|
||||
@@ -17,8 +18,10 @@ import {
|
||||
} from "@thirdweb-dev/react-core";
|
||||
import type { SmartContract } from "@thirdweb-dev/sdk";
|
||||
import type { CallOverrides, ContractInterface } from "ethers";
|
||||
import { PropsWithChildren, useContext, useState } from "react";
|
||||
import { PropsWithChildren, useState } from "react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { CustomThemeProvider } from "../../../design-system/CustomThemeProvider";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
type ActionFn = (contract: SmartContract) => any;
|
||||
|
||||
@@ -41,8 +44,12 @@ interface Web3ButtonProps<TActionFn extends ActionFn> {
|
||||
// the fn to execute
|
||||
action: TActionFn;
|
||||
type?: "button" | "submit" | "reset";
|
||||
theme?: "dark" | "light";
|
||||
theme?: "dark" | "light" | Theme;
|
||||
style?: React.CSSProperties;
|
||||
connectWallet?: Omit<
|
||||
ConnectWalletProps,
|
||||
"detailsBtn" | "hideTestnetFaucet" | "switchToActiveChain" | "theme"
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,20 +73,24 @@ interface Web3ButtonProps<TActionFn extends ActionFn> {
|
||||
*
|
||||
* @beta
|
||||
*/
|
||||
export const Web3Button = <TAction extends ActionFn>({
|
||||
contractAddress,
|
||||
onSuccess,
|
||||
onError,
|
||||
onSubmit,
|
||||
isDisabled,
|
||||
contractAbi,
|
||||
children,
|
||||
action,
|
||||
className,
|
||||
type,
|
||||
theme,
|
||||
style,
|
||||
}: PropsWithChildren<Web3ButtonProps<TAction>>) => {
|
||||
export const Web3Button = <TAction extends ActionFn>(
|
||||
props: PropsWithChildren<Web3ButtonProps<TAction>>,
|
||||
) => {
|
||||
const {
|
||||
contractAddress,
|
||||
onSuccess,
|
||||
onError,
|
||||
onSubmit,
|
||||
isDisabled,
|
||||
contractAbi,
|
||||
children,
|
||||
action,
|
||||
className,
|
||||
type,
|
||||
style,
|
||||
connectWallet,
|
||||
} = props;
|
||||
|
||||
const address = useAddress();
|
||||
const sdkChainId = useSDKChainId();
|
||||
const switchChain = useSwitchChain();
|
||||
@@ -90,13 +101,15 @@ export const Web3Button = <TAction extends ActionFn>({
|
||||
const requiresConfirmation = !useIsHeadlessWallet();
|
||||
|
||||
const { contract } = useContract(contractAddress, contractAbi || "custom");
|
||||
const thirdwebTheme = useContext(ThirdwebThemeContext);
|
||||
const themeToUse = theme || thirdwebTheme || "dark";
|
||||
const contextTheme = useTheme() as ThemeObjectOrType;
|
||||
const theme = props.theme || contextTheme || "dark";
|
||||
|
||||
const [confirmStatus, setConfirmStatus] = useState<"idle" | "waiting">(
|
||||
"idle",
|
||||
);
|
||||
|
||||
const themeType = typeof theme === "string" ? theme : theme.type;
|
||||
|
||||
const actionMutation = useMutation(
|
||||
async () => {
|
||||
invariant(contract, "contract is not ready yet");
|
||||
@@ -130,6 +143,7 @@ export const Web3Button = <TAction extends ActionFn>({
|
||||
style={style}
|
||||
theme={theme}
|
||||
className={`${className || ""} ${TW_WEB3BUTTON}--connect-wallet`}
|
||||
{...connectWallet}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -160,16 +174,16 @@ export const Web3Button = <TAction extends ActionFn>({
|
||||
if (hasMismatch && !isDisabled) {
|
||||
const _button = (
|
||||
<Button
|
||||
variant="inverted"
|
||||
variant="primary"
|
||||
type={type}
|
||||
className={`${className || ""} ${TW_WEB3BUTTON}--switch-network`}
|
||||
onClick={handleSwitchChain}
|
||||
style={{ ...btnStyle, ...style }}
|
||||
data-is-loading={confirmStatus === "waiting"}
|
||||
data-theme={theme}
|
||||
data-theme={themeType}
|
||||
>
|
||||
{confirmStatus === "waiting" ? (
|
||||
<Spinner size="sm" color={"inverted"} />
|
||||
<Spinner size="sm" color={"primaryButtonText"} />
|
||||
) : (
|
||||
"Switch Network"
|
||||
)}
|
||||
@@ -205,15 +219,15 @@ export const Web3Button = <TAction extends ActionFn>({
|
||||
) {
|
||||
button = (
|
||||
<Button
|
||||
variant="inverted"
|
||||
variant="primary"
|
||||
type={type}
|
||||
className={`${className || ""} ${TW_WEB3BUTTON}`}
|
||||
disabled
|
||||
style={{ ...btnStyle, ...style }}
|
||||
data-is-loading
|
||||
data-theme={theme}
|
||||
data-theme={themeType}
|
||||
>
|
||||
<Spinner size="md" color={"inverted"} />
|
||||
<Spinner size="md" color={"primaryButtonText"} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -222,23 +236,19 @@ export const Web3Button = <TAction extends ActionFn>({
|
||||
else {
|
||||
button = (
|
||||
<Button
|
||||
variant="inverted"
|
||||
variant="primary"
|
||||
type={type}
|
||||
className={`${className || ""} ${TW_WEB3BUTTON}`}
|
||||
onClick={() => actionMutation.mutate()}
|
||||
disabled={isDisabled}
|
||||
style={{ ...btnStyle, ...style }}
|
||||
data-is-loading="false"
|
||||
data-theme={theme}
|
||||
data-theme={themeType}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={themeToUse === "dark" ? darkTheme : lightTheme}>
|
||||
{button}
|
||||
</ThemeProvider>
|
||||
);
|
||||
return <CustomThemeProvider theme={theme}>{button}</CustomThemeProvider>;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ export function usePaperWallet() {
|
||||
return useCallback(
|
||||
async (options: { chainId?: number; email?: string } & PaperConfig) => {
|
||||
const { paperWallet } = await import(
|
||||
"../../../wallet/wallets/paperWallet"
|
||||
"../../../wallet/wallets/paper/paperWallet"
|
||||
);
|
||||
return connect(paperWallet(options), {
|
||||
chainId: options.chainId,
|
||||
|
||||
@@ -24,7 +24,7 @@ export function useWalletConnect() {
|
||||
qrModalOptions?: WC2_QRModalOptions;
|
||||
}) => {
|
||||
const { walletConnect } = await import(
|
||||
"../../../wallet/wallets/walletConnect"
|
||||
"../../../wallet/wallets/walletConnect/walletConnect"
|
||||
);
|
||||
return connect(walletConnect(options), options);
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ export { useSafe } from "./connectors/gnosis";
|
||||
export { useMagic } from "./connectors/magic";
|
||||
|
||||
export { ConnectWallet } from "../wallet/ConnectWallet/ConnectWallet";
|
||||
export { ConnectModalInline } from "../wallet/ConnectWallet/ConnectModal";
|
||||
export { ConnectModalInline } from "../wallet/ConnectWallet/Modal/ConnectModalInline";
|
||||
|
||||
export { NetworkSelector } from "../wallet/ConnectWallet/NetworkSelector";
|
||||
export type { NetworkSelectorProps } from "../wallet/ConnectWallet/NetworkSelector";
|
||||
|
||||
@@ -4,17 +4,17 @@ import {
|
||||
WalletConfig,
|
||||
} from "@thirdweb-dev/react-core";
|
||||
import { WalletUIStatesProvider } from "./wallet-ui-states-provider";
|
||||
import { ConnectModal } from "../../wallet/ConnectWallet/ConnectModal";
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
import { darkTheme, lightTheme } from "../../design-system";
|
||||
import { ConnectModal } from "../../wallet/ConnectWallet/Modal/ConnectModal";
|
||||
import { ThemeObjectOrType } from "../../design-system";
|
||||
import { PropsWithChildren } from "react";
|
||||
import type { Chain, defaultChains } from "@thirdweb-dev/chains";
|
||||
import { defaultWallets } from "../../wallet/wallets/defaultWallets";
|
||||
import { CustomThemeProvider } from "../../design-system/CustomThemeProvider";
|
||||
|
||||
interface ThirdwebProviderProps<TChains extends Chain[]>
|
||||
extends Omit<
|
||||
ThirdwebProviderCoreProps<TChains>,
|
||||
"createWalletStorage" | "supportedWallets" | "signer"
|
||||
"createWalletStorage" | "supportedWallets" | "signer" | "theme"
|
||||
> {
|
||||
/**
|
||||
* Wallets supported by the dApp
|
||||
@@ -30,6 +30,12 @@ interface ThirdwebProviderProps<TChains extends Chain[]>
|
||||
* ```
|
||||
*/
|
||||
supportedWallets?: WalletConfig<any>[];
|
||||
|
||||
/**
|
||||
* theme to use for all thirdweb components
|
||||
* @defaultValue "dark"
|
||||
*/
|
||||
theme?: ThemeObjectOrType;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,24 +63,25 @@ export const ThirdwebProvider = <
|
||||
TChains extends Chain[] = typeof defaultChains,
|
||||
>({
|
||||
supportedWallets,
|
||||
theme,
|
||||
children,
|
||||
theme: _theme,
|
||||
...restProps
|
||||
}: PropsWithChildren<ThirdwebProviderProps<TChains>>) => {
|
||||
const wallets: WalletConfig[] = supportedWallets || defaultWallets;
|
||||
const theme = _theme || "dark";
|
||||
|
||||
return (
|
||||
<WalletUIStatesProvider theme={theme}>
|
||||
<ThemeProvider theme={theme === "dark" ? darkTheme : lightTheme}>
|
||||
<WalletUIStatesProvider theme={theme} modalSize="wide">
|
||||
<CustomThemeProvider theme={theme}>
|
||||
<ThirdwebProviderCore
|
||||
theme={theme}
|
||||
supportedWallets={wallets}
|
||||
{...restProps}
|
||||
theme={typeof theme === "string" ? theme : theme.type}
|
||||
supportedWallets={wallets}
|
||||
>
|
||||
{children}
|
||||
<ConnectModal />
|
||||
</ThirdwebProviderCore>
|
||||
</ThemeProvider>
|
||||
</CustomThemeProvider>
|
||||
</WalletUIStatesProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { useState, createContext, useContext } from "react";
|
||||
import { defaultModalTitle } from "../../wallet/ConnectWallet/constants";
|
||||
import { isMobile } from "../utils/isMobile";
|
||||
import { Theme } from "../../design-system";
|
||||
import { WelcomeScreen } from "../../wallet/ConnectWallet/screens/types";
|
||||
|
||||
type BoolSetter = (value: boolean) => void;
|
||||
|
||||
type ModalConfig = {
|
||||
export type ModalConfig = {
|
||||
title: string;
|
||||
theme: "light" | "dark";
|
||||
theme: "light" | "dark" | Theme;
|
||||
data: any;
|
||||
modalSize: "wide" | "compact";
|
||||
termsOfServiceUrl?: string;
|
||||
privacyPolicyUrl?: string;
|
||||
welcomeScreen?: WelcomeScreen;
|
||||
};
|
||||
|
||||
const WalletModalOpen = /* @__PURE__ */ createContext(false);
|
||||
@@ -17,6 +25,7 @@ export const ModalConfigCtx = /* @__PURE__ */ createContext<ModalConfig>({
|
||||
title: "",
|
||||
theme: "dark",
|
||||
data: undefined,
|
||||
modalSize: "wide",
|
||||
});
|
||||
|
||||
export const SetModalConfigCtx = /* @__PURE__ */ createContext<
|
||||
@@ -24,14 +33,26 @@ export const SetModalConfigCtx = /* @__PURE__ */ createContext<
|
||||
>(() => {});
|
||||
|
||||
export const WalletUIStatesProvider = (
|
||||
props: React.PropsWithChildren<{ theme?: "light" | "dark" }>,
|
||||
props: React.PropsWithChildren<{
|
||||
theme?: "light" | "dark" | Theme;
|
||||
modalSize: "wide" | "compact";
|
||||
title?: string;
|
||||
termsOfServiceUrl?: string;
|
||||
privacyPolicyUrl?: string;
|
||||
welcomeScreen?: WelcomeScreen;
|
||||
}>,
|
||||
) => {
|
||||
const [isWalletModalOpen, setIsWalletModalOpen] = useState(false);
|
||||
const _isMobile = isMobile();
|
||||
|
||||
const [modalConfig, setModalConfig] = useState<ModalConfig>({
|
||||
title: "Choose your wallet",
|
||||
title: props.title || defaultModalTitle,
|
||||
theme: props.theme || "dark",
|
||||
data: undefined,
|
||||
modalSize: _isMobile ? "compact" : props.modalSize,
|
||||
termsOfServiceUrl: props.termsOfServiceUrl,
|
||||
privacyPolicyUrl: props.privacyPolicyUrl,
|
||||
welcomeScreen: props.welcomeScreen,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,16 +4,19 @@ export { coinbaseWallet } from "./wallet/wallets/coinbase/coinbaseWallet";
|
||||
export { frameWallet } from "./wallet/wallets/frame/frameWallet";
|
||||
export { metamaskWallet } from "./wallet/wallets/metamask/metamaskWallet";
|
||||
export { phantomWallet } from "./wallet/wallets/phantom/phantomWallet";
|
||||
export { paperWallet } from "./wallet/wallets/paperWallet";
|
||||
export { paperWallet } from "./wallet/wallets/paper/paperWallet";
|
||||
export { rainbowWallet } from "./wallet/wallets/rainbow/RainbowWallet";
|
||||
export { safeWallet } from "./wallet/wallets/safe/safeWallet";
|
||||
export { trustWallet } from "./wallet/wallets/trustWallet/TrustWallet";
|
||||
export { walletConnect } from "./wallet/wallets/walletConnect";
|
||||
export { walletConnect } from "./wallet/wallets/walletConnect/walletConnect";
|
||||
export { walletConnectV1 } from "./wallet/wallets/walletConnectV1";
|
||||
export { smartWallet } from "./wallet/wallets/smartWallet/smartWallet";
|
||||
export { magicLink } from "./wallet/wallets/magic/magicLink";
|
||||
export { zerionWallet } from "./wallet/wallets/zerion/zerionWallet";
|
||||
export { bloctoWallet } from "./wallet/wallets/blocto/bloctoWallet";
|
||||
|
||||
export { lightTheme, darkTheme } from "./design-system/index";
|
||||
export type { Theme, ThemeOverrides } from "./design-system/index";
|
||||
|
||||
// at the moment we'll re-export everything from the evm package
|
||||
export * from "./evm";
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const wait = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
@@ -1,242 +0,0 @@
|
||||
import { Modal } from "../../components/Modal";
|
||||
import { WalletSelector } from "./WalletSelector";
|
||||
import {
|
||||
WalletConfig,
|
||||
useConnectionStatus,
|
||||
useDisconnect,
|
||||
useWallet,
|
||||
useWallets,
|
||||
} from "@thirdweb-dev/react-core";
|
||||
import {
|
||||
ModalConfigCtx,
|
||||
SetModalConfigCtx,
|
||||
WalletUIStatesProvider,
|
||||
useIsWalletModalOpen,
|
||||
useSetIsWalletModalOpen,
|
||||
} from "../../evm/providers/wallet-ui-states-provider";
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
import {
|
||||
Theme,
|
||||
darkTheme,
|
||||
lightTheme,
|
||||
radius,
|
||||
spacing,
|
||||
} from "../../design-system";
|
||||
import { useState, useCallback, useEffect, useRef, useContext } from "react";
|
||||
import { GetStartedWithWallets } from "./screens/GetStartedWithWallets";
|
||||
import { reservedScreens } from "./constants";
|
||||
import { HeadlessConnectUI } from "../wallets/headlessConnectUI";
|
||||
import styled from "@emotion/styled";
|
||||
import { scrollbar } from "../../design-system/styles";
|
||||
|
||||
export const ConnectModalContent = (props: {
|
||||
screen: string | WalletConfig;
|
||||
setScreen: (screen: string | WalletConfig) => void;
|
||||
theme?: "light" | "dark";
|
||||
title?: string;
|
||||
}) => {
|
||||
const { screen, setScreen } = props;
|
||||
const modalConfig = useContext(ModalConfigCtx);
|
||||
const title = props.title || modalConfig.title;
|
||||
const theme = props.theme || modalConfig.theme;
|
||||
const walletConfigs = useWallets();
|
||||
const initialScreen =
|
||||
walletConfigs.length === 1 && !walletConfigs[0].selectUI
|
||||
? walletConfigs[0]
|
||||
: reservedScreens.main;
|
||||
|
||||
const isWalletModalOpen = useIsWalletModalOpen();
|
||||
const setIsWalletModalOpen = useSetIsWalletModalOpen();
|
||||
const connectionStatus = useConnectionStatus();
|
||||
const walletModalConfig = useContext(ModalConfigCtx);
|
||||
const setWalletModalConfig = useContext(SetModalConfigCtx);
|
||||
const disconnect = useDisconnect();
|
||||
|
||||
const handleClose = useCallback(
|
||||
(reset = true) => {
|
||||
if (reset) {
|
||||
setScreen(initialScreen);
|
||||
}
|
||||
if (connectionStatus === "connecting") {
|
||||
disconnect();
|
||||
}
|
||||
setIsWalletModalOpen(false);
|
||||
},
|
||||
[
|
||||
connectionStatus,
|
||||
setIsWalletModalOpen,
|
||||
setScreen,
|
||||
initialScreen,
|
||||
disconnect,
|
||||
],
|
||||
);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
setScreen(initialScreen);
|
||||
}, [setScreen, initialScreen]);
|
||||
|
||||
const WalletConnectUI =
|
||||
typeof screen !== "string" && (screen.connectUI || HeadlessConnectUI);
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
theme={
|
||||
typeof theme === "object"
|
||||
? theme
|
||||
: theme === "light"
|
||||
? lightTheme
|
||||
: darkTheme
|
||||
}
|
||||
>
|
||||
{screen === reservedScreens.main && (
|
||||
<WalletSelector
|
||||
title={title}
|
||||
walletConfigs={walletConfigs}
|
||||
onGetStarted={() => {
|
||||
setScreen(reservedScreens.getStarted);
|
||||
}}
|
||||
selectWallet={setScreen}
|
||||
/>
|
||||
)}
|
||||
|
||||
{screen === reservedScreens.getStarted && (
|
||||
<GetStartedWithWallets onBack={handleBack} />
|
||||
)}
|
||||
|
||||
{WalletConnectUI && (
|
||||
<WalletConnectUI
|
||||
supportedWallets={walletConfigs}
|
||||
theme={theme}
|
||||
goBack={handleBack}
|
||||
close={handleClose}
|
||||
isOpen={isWalletModalOpen}
|
||||
open={() => {
|
||||
setIsWalletModalOpen(true);
|
||||
}}
|
||||
walletConfig={screen}
|
||||
selectionData={walletModalConfig.data}
|
||||
setSelectionData={(data) => {
|
||||
setWalletModalConfig((config) => ({
|
||||
...config,
|
||||
data,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConnectModal = () => {
|
||||
const { theme } = useContext(ModalConfigCtx);
|
||||
const { screen, setScreen, initialScreen } = useScreen();
|
||||
const isWalletModalOpen = useIsWalletModalOpen();
|
||||
const setIsWalletModalOpen = useSetIsWalletModalOpen();
|
||||
const connectionStatus = useConnectionStatus();
|
||||
const disconnect = useDisconnect();
|
||||
|
||||
const wallet = useWallet();
|
||||
const isWrapperConnected = !!wallet?.getPersonalWallet();
|
||||
const prevConnectionStatus = useRef(connectionStatus);
|
||||
|
||||
const isWrapperScreen =
|
||||
typeof screen !== "string" && !!screen.personalWallets;
|
||||
|
||||
// reopen the screen to complete wrapper wallet's next step after connecting a personal wallet
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isWrapperConnected &&
|
||||
isWrapperScreen &&
|
||||
!isWalletModalOpen &&
|
||||
connectionStatus === "connected" &&
|
||||
prevConnectionStatus.current === "connecting"
|
||||
) {
|
||||
setIsWalletModalOpen(true);
|
||||
}
|
||||
|
||||
prevConnectionStatus.current = connectionStatus;
|
||||
}, [
|
||||
isWalletModalOpen,
|
||||
connectionStatus,
|
||||
setIsWalletModalOpen,
|
||||
isWrapperScreen,
|
||||
isWrapperConnected,
|
||||
]);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme === "light" ? lightTheme : darkTheme}>
|
||||
<Modal
|
||||
style={{
|
||||
maxWidth: "450px",
|
||||
maxHeight: "70vh",
|
||||
}}
|
||||
open={isWalletModalOpen}
|
||||
setOpen={(value) => {
|
||||
setIsWalletModalOpen(value);
|
||||
if (!value) {
|
||||
setScreen(initialScreen); // reset screen
|
||||
}
|
||||
if (connectionStatus === "connecting") {
|
||||
disconnect();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ConnectModalContent screen={screen} setScreen={setScreen} />
|
||||
</Modal>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConnectModalInline = (props: {
|
||||
theme?: "light" | "dark";
|
||||
title?: string;
|
||||
className?: string;
|
||||
}) => {
|
||||
const { screen, setScreen } = useScreen();
|
||||
return (
|
||||
<WalletUIStatesProvider theme={props.theme}>
|
||||
<ThemeProvider theme={props.theme === "light" ? lightTheme : darkTheme}>
|
||||
<ConnectModalInlineContainer className={props.className}>
|
||||
<ConnectModalContent
|
||||
screen={screen}
|
||||
setScreen={setScreen}
|
||||
theme={props.theme}
|
||||
title={props.title}
|
||||
/>
|
||||
</ConnectModalInlineContainer>
|
||||
</ThemeProvider>
|
||||
</WalletUIStatesProvider>
|
||||
);
|
||||
};
|
||||
|
||||
function useScreen() {
|
||||
const walletConfigs = useWallets();
|
||||
const initialScreen =
|
||||
walletConfigs.length === 1 && !walletConfigs[0].selectUI
|
||||
? walletConfigs[0]
|
||||
: reservedScreens.main;
|
||||
|
||||
const [screen, setScreen] = useState<string | WalletConfig>(initialScreen);
|
||||
return {
|
||||
screen,
|
||||
setScreen,
|
||||
initialScreen,
|
||||
};
|
||||
}
|
||||
|
||||
const ConnectModalInlineContainer = styled.div<{ theme?: Theme }>`
|
||||
background: ${(p) => p.theme.bg.base};
|
||||
padding: ${spacing.lg};
|
||||
border-radius: ${radius.xl};
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
${(p) =>
|
||||
scrollbar({
|
||||
track: "transparent",
|
||||
thumb: p.theme.bg.elevated,
|
||||
hover: p.theme.bg.highlighted,
|
||||
})}
|
||||
`;
|
||||
@@ -1,8 +1,11 @@
|
||||
import { darkTheme, iconSize, lightTheme, spacing } from "../../design-system";
|
||||
import { ConnectedWalletDetails, type DropDownPosition } from "./Details";
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
import {
|
||||
ThirdwebThemeContext,
|
||||
Theme,
|
||||
ThemeObjectOrType,
|
||||
iconSize,
|
||||
spacing,
|
||||
} from "../../design-system";
|
||||
import { ConnectedWalletDetails, type DropDownPosition } from "./Details";
|
||||
import {
|
||||
useAddress,
|
||||
useConnectionStatus,
|
||||
useLogin,
|
||||
@@ -13,6 +16,7 @@ import {
|
||||
useUser,
|
||||
useWallet,
|
||||
useWalletContext,
|
||||
useWallets,
|
||||
} from "@thirdweb-dev/react-core";
|
||||
import { useContext, useState } from "react";
|
||||
import {
|
||||
@@ -22,20 +26,26 @@ import {
|
||||
import { Button } from "../../components/buttons";
|
||||
import { Spinner } from "../../components/Spinner";
|
||||
import styled from "@emotion/styled";
|
||||
import { fadeInAnimation } from "../../components/FadeIn";
|
||||
import { LockIcon } from "./icons/LockIcon";
|
||||
import { Flex } from "../../components/basic";
|
||||
import { Container } from "../../components/basic";
|
||||
import { shortenAddress } from "../../evm/utils/addresses";
|
||||
import { SignatureModal } from "./SignatureModal";
|
||||
import type { NetworkSelectorProps } from "./NetworkSelector";
|
||||
import { defaultModalTitle } from "./constants";
|
||||
import { isMobile } from "../../evm/utils/isMobile";
|
||||
import { CustomThemeProvider } from "../../design-system/CustomThemeProvider";
|
||||
import { WelcomeScreen } from "./screens/types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { fadeInAnimation } from "../../design-system/animations";
|
||||
|
||||
type ConnectWalletProps = {
|
||||
export type ConnectWalletProps = {
|
||||
className?: string;
|
||||
theme?: "dark" | "light";
|
||||
theme?: "dark" | "light" | Theme;
|
||||
|
||||
btnTitle?: string;
|
||||
/**
|
||||
* Set a custom title for the modal
|
||||
* @default "Choose your wallet"
|
||||
* @default "Connect"
|
||||
*/
|
||||
modalTitle?: string;
|
||||
/**
|
||||
@@ -48,7 +58,9 @@ type ConnectWalletProps = {
|
||||
onLogin?: (token: string) => void;
|
||||
onLogout?: () => void;
|
||||
};
|
||||
|
||||
style?: React.CSSProperties;
|
||||
|
||||
networkSelector?: Omit<NetworkSelectorProps, "theme" | "onClose" | "chains">;
|
||||
|
||||
/**
|
||||
@@ -68,6 +80,34 @@ type ConnectWalletProps = {
|
||||
* @default false
|
||||
*/
|
||||
switchToActiveChain?: boolean;
|
||||
|
||||
/**
|
||||
* Set the size of the modal - `compact` or `wide` on desktop
|
||||
*
|
||||
* Modal size is always `compact` on mobile
|
||||
*
|
||||
* @default "wide"
|
||||
*/
|
||||
modalSize?: "compact" | "wide";
|
||||
|
||||
/**
|
||||
* If provided, Modal will show a Terms of Service message at the bottom with below link
|
||||
*/
|
||||
termsOfServiceUrl?: string;
|
||||
|
||||
/**
|
||||
* If provided, Modal will show a Privacy Policy message at the bottom with below link
|
||||
*/
|
||||
privacyPolicyUrl?: string;
|
||||
|
||||
/**
|
||||
* Customize the welcome screen
|
||||
*
|
||||
* Either provide a component to replace the default screen entirely
|
||||
*
|
||||
* or an object with title, subtitle and imgSrc to change the content of the default screen
|
||||
*/
|
||||
welcomeScreen?: WelcomeScreen;
|
||||
};
|
||||
|
||||
const TW_CONNECT_WALLET = "tw-connect-wallet";
|
||||
@@ -79,10 +119,11 @@ const TW_CONNECT_WALLET = "tw-connect-wallet";
|
||||
*/
|
||||
export const ConnectWallet: React.FC<ConnectWalletProps> = (props) => {
|
||||
const activeWallet = useWallet();
|
||||
const themeFromProvider = useContext(ThirdwebThemeContext);
|
||||
const theme = props.theme || themeFromProvider || "dark";
|
||||
const contextTheme = useTheme() as ThemeObjectOrType;
|
||||
const theme = props.theme || contextTheme || "dark";
|
||||
const connectionStatus = useConnectionStatus();
|
||||
|
||||
const walletConfigs = useWallets();
|
||||
const isLoading =
|
||||
connectionStatus === "connecting" || connectionStatus === "unknown";
|
||||
|
||||
@@ -116,7 +157,7 @@ export const ConnectWallet: React.FC<ConnectWalletProps> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme === "dark" ? darkTheme : lightTheme}>
|
||||
<CustomThemeProvider theme={theme}>
|
||||
{showSignatureModal && (
|
||||
<SignatureModal
|
||||
open={showSignatureModal}
|
||||
@@ -134,7 +175,7 @@ export const ConnectWallet: React.FC<ConnectWalletProps> = (props) => {
|
||||
className={`${props.className || ""} ${TW_CONNECT_WALLET}`}
|
||||
data-theme={theme}
|
||||
data-is-loading={isLoading}
|
||||
variant="inverted"
|
||||
variant="primary"
|
||||
type="button"
|
||||
style={{
|
||||
minWidth: "140px",
|
||||
@@ -144,16 +185,30 @@ export const ConnectWallet: React.FC<ConnectWalletProps> = (props) => {
|
||||
connectionStatus === "connecting" ? "Connecting" : btnTitle
|
||||
}
|
||||
onClick={() => {
|
||||
let modalSize = props.modalSize || "wide";
|
||||
|
||||
if (isMobile() || walletConfigs.length === 1) {
|
||||
modalSize = "compact";
|
||||
}
|
||||
|
||||
setModalConfig({
|
||||
title: props.modalTitle || "Choose your wallet",
|
||||
title: props.modalTitle || defaultModalTitle,
|
||||
theme,
|
||||
data: undefined,
|
||||
modalSize,
|
||||
termsOfServiceUrl: props.termsOfServiceUrl,
|
||||
privacyPolicyUrl: props.privacyPolicyUrl,
|
||||
welcomeScreen: props.welcomeScreen,
|
||||
});
|
||||
setIsWalletModalOpen(true);
|
||||
}}
|
||||
data-test="connect-wallet-button"
|
||||
>
|
||||
{isLoading ? <Spinner size="sm" color="inverted" /> : btnTitle}
|
||||
{isLoading ? (
|
||||
<Spinner size="sm" color="primaryButtonText" />
|
||||
) : (
|
||||
btnTitle
|
||||
)}
|
||||
</AnimatedButton>
|
||||
);
|
||||
}
|
||||
@@ -167,7 +222,6 @@ export const ConnectWallet: React.FC<ConnectWalletProps> = (props) => {
|
||||
return (
|
||||
<SwitchNetworkButton
|
||||
style={props.style}
|
||||
theme={theme}
|
||||
className={props.className}
|
||||
/>
|
||||
);
|
||||
@@ -177,7 +231,7 @@ export const ConnectWallet: React.FC<ConnectWalletProps> = (props) => {
|
||||
else if (requiresSignIn) {
|
||||
return (
|
||||
<Button
|
||||
variant="inverted"
|
||||
variant="primary"
|
||||
onClick={signIn}
|
||||
data-theme={theme}
|
||||
className={`${TW_CONNECT_WALLET}--sign-in ${
|
||||
@@ -186,8 +240,9 @@ export const ConnectWallet: React.FC<ConnectWalletProps> = (props) => {
|
||||
style={props.style}
|
||||
data-test="sign-in-button"
|
||||
>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
<Container
|
||||
flex="row"
|
||||
center="y"
|
||||
gap="sm"
|
||||
style={{
|
||||
paddingRight: spacing.xs,
|
||||
@@ -197,7 +252,7 @@ export const ConnectWallet: React.FC<ConnectWalletProps> = (props) => {
|
||||
>
|
||||
<LockIcon size={iconSize.sm} />
|
||||
<span> Sign in </span>
|
||||
</Flex>
|
||||
</Container>
|
||||
<span>{shortenAddress(address || "", true)}</span>
|
||||
</Button>
|
||||
);
|
||||
@@ -206,10 +261,10 @@ export const ConnectWallet: React.FC<ConnectWalletProps> = (props) => {
|
||||
// wallet details button
|
||||
return (
|
||||
<ConnectedWalletDetails
|
||||
theme={theme}
|
||||
networkSelector={props.networkSelector}
|
||||
dropdownPosition={props.dropdownPosition}
|
||||
className={props.className}
|
||||
theme={theme}
|
||||
style={props.style}
|
||||
detailsBtn={props.detailsBtn}
|
||||
hideTestnetFaucet={props.hideTestnetFaucet}
|
||||
@@ -222,14 +277,13 @@ export const ConnectWallet: React.FC<ConnectWalletProps> = (props) => {
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</ThemeProvider>
|
||||
</CustomThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
function SwitchNetworkButton(props: {
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
theme: "dark" | "light";
|
||||
}) {
|
||||
const { activeChain } = useWalletContext();
|
||||
const switchChain = useSwitchChain();
|
||||
@@ -240,9 +294,8 @@ function SwitchNetworkButton(props: {
|
||||
className={`${TW_CONNECT_WALLET}--switch-network ${
|
||||
props.className || ""
|
||||
}`}
|
||||
variant="inverted"
|
||||
variant="primary"
|
||||
type="button"
|
||||
data-theme={props.theme}
|
||||
data-is-loading={switching}
|
||||
data-test="switch-network-button"
|
||||
disabled={switching}
|
||||
@@ -261,7 +314,11 @@ function SwitchNetworkButton(props: {
|
||||
}}
|
||||
aria-label={switching ? "Switching Network" : undefined}
|
||||
>
|
||||
{switching ? <Spinner size="sm" color="inverted" /> : "Switch Network"}
|
||||
{switching ? (
|
||||
<Spinner size="sm" color="primaryButtonText" />
|
||||
) : (
|
||||
"Switch Network"
|
||||
)}
|
||||
</AnimatedButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,15 +40,16 @@ import {
|
||||
WalletInstance,
|
||||
} from "@thirdweb-dev/react-core";
|
||||
import { useState } from "react";
|
||||
import { fadeInAnimation } from "../../components/FadeIn";
|
||||
import { MetaMaskWallet, walletIds } from "@thirdweb-dev/wallets";
|
||||
import { Flex } from "../../components/basic";
|
||||
import { Container } from "../../components/basic";
|
||||
import { FundsIcon } from "./icons/FundsIcon";
|
||||
import { ExportLocalWallet } from "../wallets/localWallet/ExportLocalWallet";
|
||||
import { ErrorMessage } from "../../components/formElements";
|
||||
import { useWalletContext } from "@thirdweb-dev/react-core";
|
||||
import { useWalletConfig } from "@thirdweb-dev/react-core";
|
||||
import type { LocalWalletConfig } from "../wallets/localWallet/types";
|
||||
import { fadeInAnimation } from "../../design-system/animations";
|
||||
import { Text } from "../../components/text";
|
||||
|
||||
export type DropDownPosition = {
|
||||
side: "top" | "bottom" | "left" | "right";
|
||||
@@ -60,12 +61,12 @@ const TW_CONNECTED_WALLET = "tw-connected-wallet";
|
||||
export const ConnectedWalletDetails: React.FC<{
|
||||
dropdownPosition?: DropDownPosition;
|
||||
onDisconnect: () => void;
|
||||
theme: "dark" | "light";
|
||||
style?: React.CSSProperties;
|
||||
networkSelector?: Omit<NetworkSelectorProps, "theme" | "onClose" | "chains">;
|
||||
className?: string;
|
||||
detailsBtn?: () => JSX.Element;
|
||||
hideTestnetFaucet?: boolean;
|
||||
theme: "light" | "dark" | Theme;
|
||||
}> = (props) => {
|
||||
const disconnect = useDisconnect();
|
||||
const chains = useSupportedChains();
|
||||
@@ -107,7 +108,6 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
<WalletInfoButton
|
||||
type="button"
|
||||
className={`${TW_CONNECTED_WALLET} ${props.className || ""}`}
|
||||
data-theme={props.theme}
|
||||
style={props.style}
|
||||
data-test="connected-wallet-details"
|
||||
>
|
||||
@@ -117,16 +117,20 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
className={`${TW_CONNECTED_WALLET}__network-icon`}
|
||||
/>
|
||||
|
||||
<ColFlex>
|
||||
<Container flex="column" gap="xs">
|
||||
{balanceQuery.data ? (
|
||||
<WalletBalance className={`${TW_CONNECTED_WALLET}__balance`}>
|
||||
<Text
|
||||
className={`${TW_CONNECTED_WALLET}__balance`}
|
||||
size="sm"
|
||||
color="primaryText"
|
||||
weight={500}
|
||||
>
|
||||
{Number(balanceQuery.data.displayValue).toFixed(3)}{" "}
|
||||
{balanceQuery.data.symbol}
|
||||
</WalletBalance>
|
||||
</Text>
|
||||
) : (
|
||||
<Skeleton height={fontSize.sm} width="82px" />
|
||||
)}
|
||||
<Spacer y="xs" />
|
||||
|
||||
{activeWallet?.walletId === walletIds.localWallet ? (
|
||||
<ErrorMessage
|
||||
@@ -139,13 +143,17 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
Guest
|
||||
</ErrorMessage>
|
||||
) : address ? (
|
||||
<WalletAddress className={`${TW_CONNECTED_WALLET}__address`}>
|
||||
<Text
|
||||
size="xs"
|
||||
weight={500}
|
||||
className={`${TW_CONNECTED_WALLET}__address`}
|
||||
>
|
||||
{shortenString(address || "")}
|
||||
</WalletAddress>
|
||||
</Text>
|
||||
) : (
|
||||
<Skeleton height={fontSize.xs} width="88px" />
|
||||
)}
|
||||
</ColFlex>
|
||||
</Container>
|
||||
|
||||
<Img
|
||||
width={iconSize.lg}
|
||||
@@ -195,7 +203,7 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
const content = (
|
||||
<div>
|
||||
{/* Balance and Account Address */}
|
||||
<Flex gap="md">
|
||||
<Container flex="row" gap="md">
|
||||
<Img
|
||||
width={iconSize.xl}
|
||||
height={iconSize.xl}
|
||||
@@ -209,7 +217,7 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
}}
|
||||
>
|
||||
{/* row 1 */}
|
||||
<Flex gap="xs" alignItems="center">
|
||||
<Container gap="xs" flex="row" center="y">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -219,9 +227,11 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
data-test="connected-wallet-address"
|
||||
data-address={address}
|
||||
>
|
||||
<AccountAddress> {shortenString(address || "")}</AccountAddress>
|
||||
<Text color="primaryText" weight={500}>
|
||||
{" "}
|
||||
{shortenString(address || "")}
|
||||
</Text>
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
style={{
|
||||
padding: "3px",
|
||||
}}
|
||||
@@ -243,7 +253,6 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
>
|
||||
<DisconnectIconButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
disconnect();
|
||||
props.onDisconnect();
|
||||
@@ -252,10 +261,10 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
<ExitIcon size={iconSize.md} />
|
||||
</DisconnectIconButton>
|
||||
</ToolTip>
|
||||
</Flex>
|
||||
</Container>
|
||||
|
||||
{/* row 2 */}
|
||||
<AccountBalance>
|
||||
<Text weight={500} size="sm">
|
||||
{" "}
|
||||
{balanceQuery.data ? (
|
||||
Number(balanceQuery.data.displayValue).toFixed(3)
|
||||
@@ -263,9 +272,9 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
<Skeleton height="1em" width="100px" />
|
||||
)}{" "}
|
||||
{balanceQuery.data?.symbol}{" "}
|
||||
</AccountBalance>
|
||||
</Text>
|
||||
</div>
|
||||
</Flex>
|
||||
</Container>
|
||||
|
||||
<Spacer y="lg" />
|
||||
|
||||
@@ -278,7 +287,7 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
|
||||
<Spacer y="md" />
|
||||
|
||||
<Flex flexDirection="column" gap={"sm"}>
|
||||
<Container flex="column" gap="sm">
|
||||
{/* Switch to Personal Wallet for Safe */}
|
||||
{personalWallet && personalWalletConfig && (
|
||||
<WalletSwitcher
|
||||
@@ -344,9 +353,9 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
fontSize: fontSize.sm,
|
||||
}}
|
||||
>
|
||||
<SecondaryIconContainer>
|
||||
<Container flex="row" center="both" color="secondaryText">
|
||||
<FundsIcon size={iconSize.sm} />
|
||||
</SecondaryIconContainer>
|
||||
</Container>
|
||||
Request Testnet Funds
|
||||
</MenuLink>
|
||||
)}
|
||||
@@ -363,9 +372,9 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
fontSize: fontSize.sm,
|
||||
}}
|
||||
>
|
||||
<SecondaryIconContainer>
|
||||
<Container flex="row" center="both" color="secondaryText">
|
||||
<PinBottomIcon width={iconSize.sm} height={iconSize.sm} />
|
||||
</SecondaryIconContainer>
|
||||
</Container>
|
||||
Backup wallet{" "}
|
||||
</MenuButton>
|
||||
<Spacer y="sm" />
|
||||
@@ -381,7 +390,7 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
</ErrorMessage>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -389,18 +398,13 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
<>
|
||||
{isMobile() ? (
|
||||
<Modal
|
||||
size={"compact"}
|
||||
trigger={trigger}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
hideCloseIcon={true}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
minHeight: "200px",
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
<Container p="lg">{content}</Container>
|
||||
</Modal>
|
||||
) : (
|
||||
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
|
||||
@@ -428,14 +432,9 @@ export const ConnectedWalletDetails: React.FC<{
|
||||
)}
|
||||
|
||||
{showExportModal && (
|
||||
<Modal
|
||||
open={true}
|
||||
setOpen={setShowExportModal}
|
||||
style={{
|
||||
maxWidth: "480px",
|
||||
}}
|
||||
>
|
||||
<Modal size={"compact"} open={true} setOpen={setShowExportModal}>
|
||||
<ExportLocalWallet
|
||||
modalSize="compact"
|
||||
localWalletConfig={activeWalletConfig as LocalWalletConfig}
|
||||
onBack={() => {
|
||||
setShowExportModal(false);
|
||||
@@ -464,23 +463,24 @@ const dropdownContentFade = keyframes`
|
||||
const DropDownContent = /* @__PURE__ */ styled(
|
||||
/* @__PURE__ */ DropdownMenu.Content,
|
||||
)<{ theme?: Theme }>`
|
||||
width: 360px;
|
||||
width: 320px;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
border-radius: ${radius.lg};
|
||||
padding: ${spacing.lg};
|
||||
animation: ${dropdownContentFade} 400ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
will-change: transform, opacity;
|
||||
border: 1px solid ${(props) => props.theme.border.base};
|
||||
background-color: ${(props) => props.theme.bg.base};
|
||||
border: 1px solid ${(props) => props.theme.colors.borderColor};
|
||||
background-color: ${(props) => props.theme.colors.dropdownBg};
|
||||
--bg: ${(props) => props.theme.colors.dropdownBg};
|
||||
z-index: 1000000;
|
||||
line-height: 1;
|
||||
`;
|
||||
|
||||
const WalletInfoButton = styled.button<{ theme?: Theme }>`
|
||||
all: unset;
|
||||
background: ${(props) => props.theme.bg.base};
|
||||
border: 1px solid ${(props) => props.theme.border.base};
|
||||
background: ${(props) => props.theme.colors.connectedButtonBg};
|
||||
border: 1px solid ${(props) => props.theme.colors.borderColor};
|
||||
padding: ${spacing.sm} ${spacing.sm};
|
||||
border-radius: ${radius.lg};
|
||||
cursor: pointer;
|
||||
@@ -503,43 +503,13 @@ const WalletInfoButton = styled.button<{ theme?: Theme }>`
|
||||
|
||||
&:hover {
|
||||
transition: background 250ms ease;
|
||||
background: ${(props) => props.theme.bg.baseHover};
|
||||
border-color: ${(props) => props.theme.bg.highlighted};
|
||||
background: ${(props) => props.theme.colors.connectedButtonBgHover};
|
||||
}
|
||||
`;
|
||||
|
||||
const WalletAddress = styled.span<{ theme?: Theme }>`
|
||||
color: ${(props) => props.theme.text.secondary};
|
||||
font-size: ${fontSize.xs};
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
const ColFlex = styled.div<{ theme?: Theme }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const WalletBalance = styled.span<{ theme?: Theme }>`
|
||||
color: ${(props) => props.theme.text.neutral};
|
||||
font-size: ${fontSize.sm};
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
const AccountAddress = styled.span<{ theme?: Theme }>`
|
||||
font-size: ${fontSize.md};
|
||||
color: ${(props) => props.theme.text.neutral};
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
const AccountBalance = styled.span<{ theme?: Theme }>`
|
||||
font-size: ${fontSize.sm};
|
||||
color: ${(props) => props.theme.text.secondary};
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
const DropdownLabel = styled.label<{ theme?: Theme }>`
|
||||
font-size: ${fontSize.sm};
|
||||
color: ${(props) => props.theme.text.secondary};
|
||||
color: ${(props) => props.theme.colors.secondaryText};
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
@@ -547,8 +517,8 @@ const MenuButton = styled.button<{ theme?: Theme }>`
|
||||
all: unset;
|
||||
padding: ${spacing.sm} ${spacing.sm};
|
||||
border-radius: ${radius.md};
|
||||
background-color: ${(props) => props.theme.bg.base};
|
||||
border: 1px solid ${(props) => props.theme.border.elevated};
|
||||
background-color: transparent;
|
||||
border: 1px solid ${(props) => props.theme.colors.borderColor};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -556,7 +526,7 @@ const MenuButton = styled.button<{ theme?: Theme }>`
|
||||
cursor: pointer;
|
||||
font-size: ${fontSize.md};
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.text.neutral} !important;
|
||||
color: ${(props) => props.theme.colors.primaryText} !important;
|
||||
gap: ${spacing.sm};
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
line-height: 1.3;
|
||||
@@ -565,8 +535,8 @@ const MenuButton = styled.button<{ theme?: Theme }>`
|
||||
transition:
|
||||
box-shadow 250ms ease,
|
||||
border-color 250ms ease;
|
||||
border: 1px solid ${(props) => props.theme.link.primary};
|
||||
box-shadow: 0 0 0 1px ${(props) => props.theme.link.primary};
|
||||
border: 1px solid ${(props) => props.theme.colors.accentText};
|
||||
box-shadow: 0 0 0 1px ${(props) => props.theme.colors.accentText};
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
@@ -580,8 +550,8 @@ const MenuButton = styled.button<{ theme?: Theme }>`
|
||||
transition:
|
||||
box-shadow 250ms ease,
|
||||
border-color 250ms ease;
|
||||
border: 1px solid ${(props) => props.theme.text.danger};
|
||||
box-shadow: 0 0 0 1px ${(props) => props.theme.text.danger};
|
||||
border: 1px solid ${(props) => props.theme.colors.danger};
|
||||
box-shadow: 0 0 0 1px ${(props) => props.theme.colors.danger};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -598,7 +568,7 @@ export const StyledChevronRightIcon = /* @__PURE__ */ styled(
|
||||
)<{
|
||||
theme?: Theme;
|
||||
}>`
|
||||
color: ${(props) => props.theme.text.secondary};
|
||||
color: ${(props) => props.theme.colors.secondaryText};
|
||||
`;
|
||||
|
||||
const DisconnectIconButton = /* @__PURE__ */ styled(IconButton)<{
|
||||
@@ -606,20 +576,13 @@ const DisconnectIconButton = /* @__PURE__ */ styled(IconButton)<{
|
||||
}>`
|
||||
margin-right: -${spacing.xxs};
|
||||
margin-left: auto;
|
||||
color: ${(props) => props.theme.icon.secondary};
|
||||
color: ${(props) => props.theme.colors.secondaryText};
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.icon.danger};
|
||||
color: ${(props) => props.theme.colors.danger};
|
||||
background: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const SecondaryIconContainer = styled.div<{ theme?: Theme }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: ${(props) => props.theme.icon.secondary};
|
||||
`;
|
||||
|
||||
function WalletSwitcher({
|
||||
wallet,
|
||||
onSwitch,
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import { Modal } from "../../../components/Modal";
|
||||
import { WalletSelector } from "../WalletSelector";
|
||||
import {
|
||||
WalletConfig,
|
||||
useConnectionStatus,
|
||||
useDisconnect,
|
||||
useWallet,
|
||||
useWallets,
|
||||
} from "@thirdweb-dev/react-core";
|
||||
import {
|
||||
ModalConfigCtx,
|
||||
SetModalConfigCtx,
|
||||
useIsWalletModalOpen,
|
||||
useSetIsWalletModalOpen,
|
||||
} from "../../../evm/providers/wallet-ui-states-provider";
|
||||
import { useCallback, useEffect, useContext } from "react";
|
||||
import { reservedScreens, compactmodalMaxHeight } from "../constants";
|
||||
import { HeadlessConnectUI } from "../../wallets/headlessConnectUI";
|
||||
import styled from "@emotion/styled";
|
||||
import { Container, noScrollBar } from "../../../components/basic";
|
||||
import { ScreenContext, useScreen } from "./screen";
|
||||
import { StartScreen } from "../screens/StartScreen";
|
||||
import { CustomThemeProvider } from "../../../design-system/CustomThemeProvider";
|
||||
import { Theme } from "../../../design-system";
|
||||
|
||||
export const ConnectModalContent = (props: {
|
||||
screen: string | WalletConfig;
|
||||
initialScreen: string | WalletConfig;
|
||||
setScreen: (screen: string | WalletConfig) => void;
|
||||
}) => {
|
||||
const { screen, setScreen, initialScreen } = props;
|
||||
|
||||
const walletConfigs = useWallets();
|
||||
const connectionStatus = useConnectionStatus();
|
||||
const disconnect = useDisconnect();
|
||||
|
||||
const isWalletModalOpen = useIsWalletModalOpen();
|
||||
const setIsWalletModalOpen = useSetIsWalletModalOpen();
|
||||
|
||||
const modalConfig = useContext(ModalConfigCtx);
|
||||
const setModalConfig = useContext(SetModalConfigCtx);
|
||||
|
||||
const title = modalConfig.title;
|
||||
const theme = modalConfig.theme;
|
||||
const modalSize = modalConfig.modalSize;
|
||||
const isWideModal = modalSize === "wide";
|
||||
|
||||
const handleClose = useCallback(
|
||||
(reset = true) => {
|
||||
if (reset) {
|
||||
setScreen(initialScreen);
|
||||
}
|
||||
if (connectionStatus === "connecting") {
|
||||
disconnect();
|
||||
}
|
||||
setIsWalletModalOpen(false);
|
||||
},
|
||||
[
|
||||
connectionStatus,
|
||||
setIsWalletModalOpen,
|
||||
setScreen,
|
||||
initialScreen,
|
||||
disconnect,
|
||||
],
|
||||
);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
setScreen(initialScreen);
|
||||
if (connectionStatus === "connecting") {
|
||||
disconnect();
|
||||
}
|
||||
}, [setScreen, initialScreen, connectionStatus, disconnect]);
|
||||
|
||||
const walletList = (
|
||||
<WalletSelector
|
||||
title={title}
|
||||
walletConfigs={walletConfigs}
|
||||
onGetStarted={() => {
|
||||
setScreen(reservedScreens.getStarted);
|
||||
}}
|
||||
selectWallet={setScreen}
|
||||
/>
|
||||
);
|
||||
|
||||
const getStarted = <StartScreen />;
|
||||
|
||||
const getWalletUI = (walletConfig: WalletConfig) => {
|
||||
const ConnectUI = walletConfig.connectUI || HeadlessConnectUI;
|
||||
|
||||
return (
|
||||
<ConnectUI
|
||||
supportedWallets={walletConfigs}
|
||||
theme={typeof theme === "string" ? theme : theme.type}
|
||||
goBack={handleBack}
|
||||
close={handleClose}
|
||||
isOpen={isWalletModalOpen}
|
||||
open={() => {
|
||||
setIsWalletModalOpen(true);
|
||||
}}
|
||||
walletConfig={walletConfig}
|
||||
modalSize={modalConfig.modalSize}
|
||||
selectionData={modalConfig.data}
|
||||
setSelectionData={(data) => {
|
||||
setModalConfig((config) => ({
|
||||
...config,
|
||||
data,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenContext.Provider value={screen}>
|
||||
{isWideModal ? (
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "300px 1fr",
|
||||
}}
|
||||
>
|
||||
<LeftContainer> {walletList} </LeftContainer>
|
||||
<Container flex="column" scrollY relative>
|
||||
{screen === reservedScreens.main && <>{getStarted}</>}
|
||||
{screen === reservedScreens.getStarted && getStarted}
|
||||
{typeof screen !== "string" && getWalletUI(screen)}
|
||||
</Container>
|
||||
</div>
|
||||
) : (
|
||||
<Container
|
||||
flex="column"
|
||||
scrollY
|
||||
relative
|
||||
style={{
|
||||
maxHeight: compactmodalMaxHeight,
|
||||
}}
|
||||
>
|
||||
{screen === reservedScreens.main && walletList}
|
||||
{screen === reservedScreens.getStarted && getStarted}
|
||||
{typeof screen !== "string" && getWalletUI(screen)}
|
||||
</Container>
|
||||
)}
|
||||
</ScreenContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConnectModal = () => {
|
||||
const { theme, modalSize } = useContext(ModalConfigCtx);
|
||||
const { screen, setScreen, initialScreen } = useScreen();
|
||||
const isWalletModalOpen = useIsWalletModalOpen();
|
||||
const setIsWalletModalOpen = useSetIsWalletModalOpen();
|
||||
const connectionStatus = useConnectionStatus();
|
||||
const disconnect = useDisconnect();
|
||||
|
||||
const wallet = useWallet();
|
||||
const isWrapperConnected = !!wallet?.getPersonalWallet();
|
||||
|
||||
const isWrapperScreen =
|
||||
typeof screen !== "string" && !!screen.personalWallets;
|
||||
|
||||
// reopen the screen to complete wrapper wallet's next step after connecting a personal wallet
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isWrapperConnected &&
|
||||
isWrapperScreen &&
|
||||
!isWalletModalOpen &&
|
||||
connectionStatus === "connected"
|
||||
) {
|
||||
setIsWalletModalOpen(true);
|
||||
}
|
||||
}, [
|
||||
isWalletModalOpen,
|
||||
connectionStatus,
|
||||
setIsWalletModalOpen,
|
||||
isWrapperScreen,
|
||||
isWrapperConnected,
|
||||
]);
|
||||
|
||||
return (
|
||||
<CustomThemeProvider theme={theme}>
|
||||
<Modal
|
||||
size={modalSize}
|
||||
open={isWalletModalOpen}
|
||||
setOpen={(value) => {
|
||||
setIsWalletModalOpen(value);
|
||||
if (!value) {
|
||||
setScreen(initialScreen); // reset screen
|
||||
}
|
||||
if (connectionStatus === "connecting") {
|
||||
disconnect();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ConnectModalContent
|
||||
initialScreen={initialScreen}
|
||||
screen={screen}
|
||||
setScreen={setScreen}
|
||||
/>
|
||||
</Modal>
|
||||
</CustomThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const LeftContainer = /* @__PURE__ */ styled.div<{
|
||||
theme?: Theme;
|
||||
}>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
${noScrollBar}
|
||||
position: relative;
|
||||
border-right: 1px solid ${(p) => p.theme.colors.separatorLine};
|
||||
`;
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Theme } from "../../../design-system";
|
||||
import styled from "@emotion/styled";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import { CrossContainer } from "../../../components/Modal";
|
||||
import { IconButton } from "../../../components/buttons";
|
||||
import { iconSize, radius, shadow } from "../../../design-system";
|
||||
import { WalletUIStatesProvider } from "../../../evm/providers/wallet-ui-states-provider";
|
||||
import {
|
||||
widemodalMaxHeight,
|
||||
modalMaxWidthCompact,
|
||||
modalMaxWidthWide,
|
||||
} from "../constants";
|
||||
import { ConnectModalContent } from "./ConnectModal";
|
||||
import { useScreen } from "./screen";
|
||||
import { isMobile } from "../../../evm/utils/isMobile";
|
||||
import { useWallets } from "@thirdweb-dev/react-core";
|
||||
import { DynamicHeight } from "../../../components/DynamicHeight";
|
||||
import { CustomThemeProvider } from "../../../design-system/CustomThemeProvider";
|
||||
import { WelcomeScreen } from "../screens/types";
|
||||
|
||||
export const ConnectModalInline = (props: {
|
||||
theme: "light" | "dark" | Theme;
|
||||
title?: string;
|
||||
className?: string;
|
||||
modalSize: "wide" | "compact";
|
||||
termsOfServiceUrl?: string;
|
||||
privacyPolicyUrl?: string;
|
||||
welcomeScreen?: WelcomeScreen;
|
||||
}) => {
|
||||
const { screen, setScreen, initialScreen } = useScreen();
|
||||
const walletConfigs = useWallets();
|
||||
const modalSize =
|
||||
isMobile() || walletConfigs.length === 1 ? "compact" : props.modalSize;
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<ConnectModalContent
|
||||
initialScreen={initialScreen}
|
||||
screen={screen}
|
||||
setScreen={setScreen}
|
||||
/>
|
||||
|
||||
{/* close icon */}
|
||||
<CrossContainer>
|
||||
<IconButton type="button" aria-label="Close">
|
||||
<Cross2Icon
|
||||
style={{
|
||||
width: iconSize.md,
|
||||
height: iconSize.md,
|
||||
color: "inherit",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</CrossContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<WalletUIStatesProvider
|
||||
theme={props.theme}
|
||||
modalSize={modalSize}
|
||||
title={props.title}
|
||||
termsOfServiceUrl={props.termsOfServiceUrl}
|
||||
privacyPolicyUrl={props.privacyPolicyUrl}
|
||||
welcomeScreen={props.welcomeScreen}
|
||||
>
|
||||
<CustomThemeProvider theme={props.theme}>
|
||||
<ConnectModalInlineContainer
|
||||
className={props.className}
|
||||
style={{
|
||||
height: modalSize === "compact" ? "auto" : widemodalMaxHeight,
|
||||
maxWidth:
|
||||
modalSize === "compact"
|
||||
? modalMaxWidthCompact
|
||||
: modalMaxWidthWide,
|
||||
}}
|
||||
>
|
||||
{props.modalSize === "compact" ? (
|
||||
<DynamicHeight> {content} </DynamicHeight>
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
</ConnectModalInlineContainer>
|
||||
</CustomThemeProvider>
|
||||
</WalletUIStatesProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const ConnectModalInlineContainer = styled.div<{ theme?: Theme }>`
|
||||
background: ${(p) => p.theme.colors.modalBg};
|
||||
color: ${(p) => p.theme.colors.primaryText};
|
||||
transition: background 0.2s ease;
|
||||
border-radius: ${radius.xl};
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
box-shadow: ${shadow.lg};
|
||||
position: relative;
|
||||
border: 1px solid ${(p) => p.theme.colors.borderColor};
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
font-family: ${(p) => p.theme.fontFamily};
|
||||
& *::selection {
|
||||
background-color: ${(p) => p.theme.colors.primaryText};
|
||||
color: ${(p) => p.theme.colors.modalBg};
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Container } from "../../../components/basic";
|
||||
import { Link, Text } from "../../../components/text";
|
||||
|
||||
export function TOS(props: {
|
||||
termsOfServiceUrl?: string;
|
||||
privacyPolicyUrl?: string;
|
||||
}) {
|
||||
const { termsOfServiceUrl, privacyPolicyUrl } = props;
|
||||
|
||||
if (!termsOfServiceUrl && !privacyPolicyUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bothGiven = termsOfServiceUrl && privacyPolicyUrl;
|
||||
|
||||
return (
|
||||
<Container flex="row" center="x">
|
||||
<Text
|
||||
size="xs"
|
||||
multiline
|
||||
balance
|
||||
inline
|
||||
center
|
||||
style={{
|
||||
maxWidth: "250px",
|
||||
}}
|
||||
>
|
||||
By connecting, you agree to the{" "}
|
||||
{termsOfServiceUrl && (
|
||||
<Link
|
||||
inline
|
||||
size="xs"
|
||||
href={termsOfServiceUrl}
|
||||
target="_blank"
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
Terms of Service{" "}
|
||||
</Link>
|
||||
)}
|
||||
{bothGiven && "& "}
|
||||
{privacyPolicyUrl && (
|
||||
<Link inline size="xs" href={privacyPolicyUrl} target="_blank">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
)}
|
||||
</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { WalletConfig, useWallets } from "@thirdweb-dev/react-core";
|
||||
import { createContext, useState, useContext } from "react";
|
||||
import { reservedScreens } from "../constants";
|
||||
|
||||
type Screen = string | WalletConfig;
|
||||
|
||||
export const ScreenContext = /* @__PURE__ */ createContext<Screen | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export function useScreen() {
|
||||
const walletConfigs = useWallets();
|
||||
const initialScreen =
|
||||
walletConfigs.length === 1 && !walletConfigs[0].selectUI
|
||||
? walletConfigs[0]
|
||||
: reservedScreens.main;
|
||||
|
||||
const [screen, setScreen] = useState<string | WalletConfig>(initialScreen);
|
||||
return {
|
||||
screen,
|
||||
setScreen,
|
||||
initialScreen,
|
||||
};
|
||||
}
|
||||
|
||||
export function useScreenContext() {
|
||||
const screen = useContext(ScreenContext);
|
||||
if (!screen) {
|
||||
throw new Error(
|
||||
"useScreenContext must be used within a <ScreenProvider />",
|
||||
);
|
||||
}
|
||||
return screen;
|
||||
}
|
||||
@@ -1,44 +1,36 @@
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
import { ChainIcon } from "../../components/ChainIcon";
|
||||
import { Modal } from "../../components/Modal";
|
||||
import { Spacer } from "../../components/Spacer";
|
||||
import { Spinner } from "../../components/Spinner";
|
||||
import { Input } from "../../components/formElements";
|
||||
import {
|
||||
darkTheme,
|
||||
fontSize,
|
||||
iconSize,
|
||||
lightTheme,
|
||||
media,
|
||||
radius,
|
||||
spacing,
|
||||
Theme,
|
||||
ThemeObjectOrType,
|
||||
} from "../../design-system";
|
||||
import { scrollbar } from "../../design-system/styles";
|
||||
import styled from "@emotion/styled";
|
||||
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||
import * as Tabs from "@radix-ui/react-tabs";
|
||||
import {
|
||||
ThirdwebThemeContext,
|
||||
useChainId,
|
||||
useSupportedChains,
|
||||
useSwitchChain,
|
||||
} from "@thirdweb-dev/react-core";
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useContext,
|
||||
useDeferredValue,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import { memo, useCallback, useDeferredValue, useMemo, useState } from "react";
|
||||
import type { Chain } from "@thirdweb-dev/chains";
|
||||
import Fuse from "fuse.js";
|
||||
import { Button } from "../../components/buttons";
|
||||
import { isMobile } from "../../evm/utils/isMobile";
|
||||
import { useEffect } from "react";
|
||||
import { Flex } from "../../components/basic";
|
||||
import { SecondaryText } from "../../components/text";
|
||||
import { Container } from "../../components/basic";
|
||||
import { Text } from "../../components/text";
|
||||
import { ModalTitle } from "../../components/modalElements";
|
||||
import { CustomThemeProvider } from "../../design-system/CustomThemeProvider";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
type RenderChain = React.FC<{
|
||||
chain: Chain;
|
||||
@@ -49,7 +41,7 @@ type RenderChain = React.FC<{
|
||||
}>;
|
||||
|
||||
export type NetworkSelectorProps = {
|
||||
theme?: "dark" | "light";
|
||||
theme: "dark" | "light" | Theme;
|
||||
onClose?: () => void;
|
||||
chains?: Chain[];
|
||||
popularChains?: Chain[];
|
||||
@@ -76,8 +68,8 @@ const fuseConfig = {
|
||||
export const NetworkSelector: React.FC<NetworkSelectorProps> = (props) => {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const deferredSearchTerm = useDeferredValue(searchTerm);
|
||||
const themeFromProvider = useContext(ThirdwebThemeContext);
|
||||
const theme = props.theme || themeFromProvider || "dark";
|
||||
const themeFromContext = useTheme() as ThemeObjectOrType;
|
||||
const theme = props.theme || themeFromContext || "dark";
|
||||
const supportedChains = useSupportedChains();
|
||||
const chains = props.chains || supportedChains;
|
||||
|
||||
@@ -142,146 +134,148 @@ export const NetworkSelector: React.FC<NetworkSelectorProps> = (props) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme === "dark" ? darkTheme : lightTheme}>
|
||||
<CustomThemeProvider theme={theme}>
|
||||
<Modal
|
||||
size={"compact"}
|
||||
open={true}
|
||||
setOpen={(value) => {
|
||||
if (!value && onClose) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title="Select Network"
|
||||
style={{
|
||||
maxWidth: "480px",
|
||||
paddingBottom: props.onCustomClick ? spacing.md : "0px",
|
||||
}}
|
||||
>
|
||||
<Spacer y="xl" />
|
||||
<Container p="lg">
|
||||
<ModalTitle>Select Network</ModalTitle>
|
||||
<Spacer y="xl" />
|
||||
|
||||
<Tabs.Root className="TabsRoot" defaultValue="all">
|
||||
<Tabs.List
|
||||
className="TabsList"
|
||||
aria-label="Manage your account"
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: spacing.xxs,
|
||||
}}
|
||||
>
|
||||
<TabButton className="TabsTrigger" value="all">
|
||||
All
|
||||
</TabButton>
|
||||
<TabButton className="TabsTrigger" value="mainnet">
|
||||
Mainnets
|
||||
</TabButton>
|
||||
<TabButton className="TabsTrigger" value="testnet">
|
||||
Testnets
|
||||
</TabButton>
|
||||
</Tabs.List>
|
||||
|
||||
<Spacer y="lg" />
|
||||
|
||||
{/* Search */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<StyledMagnifyingGlassIcon
|
||||
width={iconSize.md}
|
||||
height={iconSize.md}
|
||||
/>
|
||||
|
||||
<SearchInput
|
||||
<Tabs.Root className="TabsRoot" defaultValue="all">
|
||||
<Tabs.List
|
||||
className="TabsList"
|
||||
aria-label="Manage your account"
|
||||
style={{
|
||||
boxShadow: "none",
|
||||
display: "flex",
|
||||
gap: spacing.xxs,
|
||||
}}
|
||||
tabIndex={isMobile() ? -1 : 0}
|
||||
variant="secondary"
|
||||
placeholder="Search Network or Chain ID"
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<TabButton className="TabsTrigger" value="all">
|
||||
All
|
||||
</TabButton>
|
||||
<TabButton className="TabsTrigger" value="mainnet">
|
||||
Mainnets
|
||||
</TabButton>
|
||||
<TabButton className="TabsTrigger" value="testnet">
|
||||
Testnets
|
||||
</TabButton>
|
||||
</Tabs.List>
|
||||
|
||||
{/* Searching Spinner */}
|
||||
{deferredSearchTerm !== searchTerm && (
|
||||
<div
|
||||
<Spacer y="lg" />
|
||||
|
||||
{/* Search */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<StyledMagnifyingGlassIcon
|
||||
width={iconSize.md}
|
||||
height={iconSize.md}
|
||||
/>
|
||||
|
||||
<Input
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: spacing.md,
|
||||
padding: `${spacing.sm} ${spacing.md} ${spacing.sm} 60px`,
|
||||
}}
|
||||
>
|
||||
<Spinner size="md" color="link" />
|
||||
</div>
|
||||
tabIndex={-1}
|
||||
variant="outline"
|
||||
placeholder="Search Network or Chain ID"
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Searching Spinner */}
|
||||
{deferredSearchTerm !== searchTerm && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: spacing.md,
|
||||
}}
|
||||
>
|
||||
<Spinner size="md" color="accentText" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Spacer y="lg" />
|
||||
|
||||
<Tabs.Content className="TabsContent" value="all">
|
||||
<NetworkTab
|
||||
allChains={allChains}
|
||||
type="all"
|
||||
popularChains={popularChains}
|
||||
recentChains={recentChains}
|
||||
onSwitch={handleSwitch}
|
||||
renderChain={props.renderChain}
|
||||
close={props.onClose}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content className="TabsContent" value="mainnet">
|
||||
<NetworkTab
|
||||
allChains={allChains}
|
||||
type="mainnet"
|
||||
popularChains={popularChains}
|
||||
recentChains={recentChains}
|
||||
onSwitch={handleSwitch}
|
||||
renderChain={props.renderChain}
|
||||
close={props.onClose}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content className="TabsContent" value="testnet">
|
||||
<NetworkTab
|
||||
allChains={allChains}
|
||||
type="testnet"
|
||||
popularChains={popularChains}
|
||||
recentChains={recentChains}
|
||||
onSwitch={handleSwitch}
|
||||
renderChain={props.renderChain}
|
||||
close={props.onClose}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
{onCustomClick && (
|
||||
<>
|
||||
<Spacer y="sm" />
|
||||
<Button
|
||||
fullWidth
|
||||
variant="link"
|
||||
onClick={() => {
|
||||
onCustomClick();
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
display: "flex",
|
||||
fontSize: fontSize.sm,
|
||||
boxShadow: "none",
|
||||
}}
|
||||
>
|
||||
Add Custom Network
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Spacer y="lg" />
|
||||
|
||||
<Tabs.Content className="TabsContent" value="all">
|
||||
<NetworkTab
|
||||
allChains={allChains}
|
||||
type="all"
|
||||
popularChains={popularChains}
|
||||
recentChains={recentChains}
|
||||
onSwitch={handleSwitch}
|
||||
renderChain={props.renderChain}
|
||||
close={props.onClose}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content className="TabsContent" value="mainnet">
|
||||
<NetworkTab
|
||||
allChains={allChains}
|
||||
type="mainnet"
|
||||
popularChains={popularChains}
|
||||
recentChains={recentChains}
|
||||
onSwitch={handleSwitch}
|
||||
renderChain={props.renderChain}
|
||||
close={props.onClose}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content className="TabsContent" value="testnet">
|
||||
<NetworkTab
|
||||
allChains={allChains}
|
||||
type="testnet"
|
||||
popularChains={popularChains}
|
||||
recentChains={recentChains}
|
||||
onSwitch={handleSwitch}
|
||||
renderChain={props.renderChain}
|
||||
close={props.onClose}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
{onCustomClick && (
|
||||
<>
|
||||
<Spacer y="sm" />
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => {
|
||||
onCustomClick();
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
fontSize: fontSize.sm,
|
||||
boxShadow: "none",
|
||||
}}
|
||||
>
|
||||
Add Custom Network
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Tabs.Root>
|
||||
</Tabs.Root>
|
||||
</Container>
|
||||
</Modal>
|
||||
</ThemeProvider>
|
||||
</CustomThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -422,16 +416,16 @@ const NetworkList = /* @__PURE__ */ memo(function NetworkList(props: {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Flex
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
<Container
|
||||
flex="row"
|
||||
center="both"
|
||||
style={{
|
||||
height: "250px",
|
||||
}}
|
||||
>
|
||||
{/* Don't put a spinner here - it's gonna freeze */}
|
||||
<SecondaryText>Loading</SecondaryText>
|
||||
</Flex>
|
||||
<Text>Loading</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -491,15 +485,17 @@ const NetworkList = /* @__PURE__ */ memo(function NetworkList(props: {
|
||||
>
|
||||
{confirming && (
|
||||
<>
|
||||
<ConfirmMessage>Confirm in Wallet</ConfirmMessage>
|
||||
<Spinner size="sm" color="link" />
|
||||
<Text size="sm" color="accentText" weight={500}>
|
||||
Confirm in Wallet
|
||||
</Text>
|
||||
<Spinner size="sm" color="accentText" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{switchingFailed && (
|
||||
<ErrorMessage>
|
||||
Error: Could not Switch Network
|
||||
</ErrorMessage>
|
||||
<Text size="sm" color="danger" weight={500}>
|
||||
Failed to Switch Network
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -520,7 +516,7 @@ const TabButton = /* @__PURE__ */ styled(/* @__PURE__ */ Tabs.Trigger)<{
|
||||
all: unset;
|
||||
font-size: ${fontSize.md};
|
||||
font-weight: 500;
|
||||
color: ${(p) => p.theme.text.secondary};
|
||||
color: ${(p) => p.theme.colors.secondaryText};
|
||||
cursor: pointer;
|
||||
padding: ${spacing.sm} ${spacing.sm};
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
@@ -529,14 +525,14 @@ const TabButton = /* @__PURE__ */ styled(/* @__PURE__ */ Tabs.Trigger)<{
|
||||
background 0.2s ease,
|
||||
color 0.2s ease;
|
||||
&[data-state="active"] {
|
||||
background: ${(p) => p.theme.bg.elevated};
|
||||
color: ${(p) => p.theme.text.neutral};
|
||||
background: ${(p) => p.theme.colors.secondaryButtonBg};
|
||||
color: ${(p) => p.theme.colors.primaryText};
|
||||
}
|
||||
`;
|
||||
|
||||
const SectionLabel = styled.p<{ theme?: Theme }>`
|
||||
font-size: ${fontSize.sm};
|
||||
color: ${(p) => p.theme.text.secondary};
|
||||
color: ${(p) => p.theme.colors.secondaryText};
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
@@ -551,8 +547,8 @@ const ScrollContainer = styled.div<{ theme?: Theme }>`
|
||||
${(p) =>
|
||||
scrollbar({
|
||||
track: "transparent",
|
||||
thumb: p.theme.bg.elevated,
|
||||
hover: p.theme.bg.highlighted,
|
||||
thumb: p.theme.colors.scrollbarBg,
|
||||
hover: p.theme.colors.scrollbarBg,
|
||||
})}
|
||||
`;
|
||||
|
||||
@@ -577,12 +573,12 @@ const NetworkButton = styled.button<{ theme?: Theme }>`
|
||||
border-radius: ${radius.md};
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
background: ${(p) => p.theme.bg.elevated};
|
||||
color: ${(p) => p.theme.text.neutral};
|
||||
background: ${(p) => p.theme.colors.secondaryButtonBg};
|
||||
color: ${(p) => p.theme.colors.primaryText};
|
||||
font-weight: 600;
|
||||
font-size: ${fontSize.md};
|
||||
&:hover {
|
||||
background: ${(p) => p.theme.bg.highlighted};
|
||||
background: ${(p) => p.theme.colors.secondaryButtonHoverBg};
|
||||
}
|
||||
|
||||
${media.mobile} {
|
||||
@@ -593,21 +589,7 @@ const NetworkButton = styled.button<{ theme?: Theme }>`
|
||||
const StyledMagnifyingGlassIcon = /* @__PURE__ */ styled(MagnifyingGlassIcon)<{
|
||||
theme?: Theme;
|
||||
}>`
|
||||
color: ${(p) => p.theme.text.secondary};
|
||||
color: ${(p) => p.theme.colors.secondaryText};
|
||||
position: absolute;
|
||||
left: 18px;
|
||||
`;
|
||||
|
||||
const SearchInput = /* @__PURE__ */ styled(Input)<{ theme?: Theme }>`
|
||||
padding: ${spacing.sm} ${spacing.md} ${spacing.sm} 60px;
|
||||
`;
|
||||
|
||||
const ConfirmMessage = styled.div<{ theme?: Theme }>`
|
||||
font-size: ${fontSize.sm};
|
||||
color: ${(p) => p.theme.link.primary};
|
||||
`;
|
||||
|
||||
const ErrorMessage = styled.div<{ theme?: Theme }>`
|
||||
font-size: ${fontSize.sm};
|
||||
color: ${(p) => p.theme.text.danger};
|
||||
`;
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { Modal } from "../../components/Modal";
|
||||
import { iconSize, media, spacing } from "../../design-system";
|
||||
import {
|
||||
HelperLink,
|
||||
ModalDescription,
|
||||
ModalTitle,
|
||||
} from "../../components/modalElements";
|
||||
import { ModalTitle } from "../../components/modalElements";
|
||||
import { useWalletConfig } from "@thirdweb-dev/react-core";
|
||||
import { Img } from "../../components/Img";
|
||||
import { Spacer } from "../../components/Spacer";
|
||||
import { Spinner } from "../../components/Spinner";
|
||||
import styled from "@emotion/styled";
|
||||
import { Container } from "../../components/basic";
|
||||
import { Text } from "../../components/text";
|
||||
import { WalletLogoSpinner } from "./screens/WalletLogoSpinner";
|
||||
|
||||
export const SignatureModal: React.FC<{
|
||||
open: boolean;
|
||||
@@ -18,66 +13,30 @@ export const SignatureModal: React.FC<{
|
||||
const walletConfig = useWalletConfig();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={props.open}
|
||||
style={{
|
||||
maxWidth: "450px",
|
||||
}}
|
||||
setOpen={props.setOpen}
|
||||
>
|
||||
<Container>
|
||||
{walletConfig && (
|
||||
<Img
|
||||
height={iconSize.xl}
|
||||
src={walletConfig.meta.iconURL}
|
||||
width={iconSize.xl}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Modal size="compact" open={props.open} setOpen={props.setOpen}>
|
||||
<Container p="lg">
|
||||
<Spacer y="xl" />
|
||||
<Container flex="column" center="x">
|
||||
{walletConfig && (
|
||||
<WalletLogoSpinner
|
||||
error={false}
|
||||
onRetry={() => {}}
|
||||
iconUrl={walletConfig.meta.iconURL}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Spacer y="xxl" />
|
||||
|
||||
<TitleContainer>
|
||||
<ModalTitle>Signature Request</ModalTitle>
|
||||
<Spinner size="md" color="link" />
|
||||
</TitleContainer>
|
||||
<Spacer y="md" />
|
||||
|
||||
<Spacer y="md" />
|
||||
<Text multiline center>
|
||||
Sign the signature request <br /> in your wallet
|
||||
</Text>
|
||||
|
||||
<Desc>Sign the signature request pop-up in your wallet</Desc>
|
||||
|
||||
<Spacer y="xxl" />
|
||||
|
||||
<HelperLink target="_blank" href="https://support.thirdweb.com/contact">
|
||||
Having troubles connecting to wallet
|
||||
</HelperLink>
|
||||
<Spacer y="lg" />
|
||||
</Container>
|
||||
</Container>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${spacing.sm};
|
||||
|
||||
${media.mobile} {
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: ${spacing.md};
|
||||
}
|
||||
`;
|
||||
|
||||
const Desc = /* @__PURE__ */ styled(ModalDescription)`
|
||||
${media.mobile} {
|
||||
max-width: 240px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
${media.mobile} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
import { useContext } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { Img } from "../../components/Img";
|
||||
import { Spacer } from "../../components/Spacer";
|
||||
import { Flex } from "../../components/basic";
|
||||
import { Button } from "../../components/buttons";
|
||||
import { HelperLink, ModalTitle } from "../../components/modalElements";
|
||||
import {
|
||||
fontSize,
|
||||
iconSize,
|
||||
radius,
|
||||
spacing,
|
||||
Theme,
|
||||
} from "../../design-system";
|
||||
noScrollBar,
|
||||
ModalHeader,
|
||||
Container,
|
||||
ScreenBottomContainer,
|
||||
Line,
|
||||
} from "../../components/basic";
|
||||
import { Button } from "../../components/buttons";
|
||||
import { ModalTitle } from "../../components/modalElements";
|
||||
import { iconSize, radius, spacing, Theme } from "../../design-system";
|
||||
import styled from "@emotion/styled";
|
||||
import { WalletConfig } from "@thirdweb-dev/react-core";
|
||||
import {
|
||||
WalletConfig,
|
||||
useConnectionStatus,
|
||||
useDisconnect,
|
||||
} from "@thirdweb-dev/react-core";
|
||||
import { walletIds } from "@thirdweb-dev/wallets";
|
||||
import {
|
||||
ModalConfigCtx,
|
||||
SetModalConfigCtx,
|
||||
} from "../../evm/providers/wallet-ui-states-provider";
|
||||
import { TWIcon } from "./icons/twIcon";
|
||||
import { Link, Text } from "../../components/text";
|
||||
import { Spacer } from "../../components/Spacer";
|
||||
import { TextDivider } from "../../components/TextDivider";
|
||||
import { TOS } from "./Modal/TOS";
|
||||
|
||||
export const WalletSelector: React.FC<{
|
||||
walletConfigs: WalletConfig[];
|
||||
@@ -25,104 +33,249 @@ export const WalletSelector: React.FC<{
|
||||
onGetStarted: () => void;
|
||||
title: string;
|
||||
}> = (props) => {
|
||||
const localWalletInfo = props.walletConfigs.find(
|
||||
const modalConfig = useContext(ModalConfigCtx);
|
||||
const isCompact = modalConfig.modalSize === "compact";
|
||||
const { termsOfServiceUrl, privacyPolicyUrl } = modalConfig;
|
||||
const [isWalletGroupExpanded, setIsWalletGroupExpanded] = useState(false);
|
||||
const disconnect = useDisconnect();
|
||||
const connectionStatus = useConnectionStatus();
|
||||
|
||||
const localWalletConfig = props.walletConfigs.find(
|
||||
(w) => w.id === walletIds.localWallet,
|
||||
);
|
||||
const walletConfigs = props.walletConfigs.filter(
|
||||
|
||||
const nonLocalWalletConfigs = props.walletConfigs.filter(
|
||||
(w) => w.id !== walletIds.localWallet,
|
||||
);
|
||||
|
||||
const showGetStarted = !localWalletInfo && !!props.walletConfigs[0].meta.urls;
|
||||
const socialWallets = nonLocalWalletConfigs.filter(
|
||||
(w) => w.category === "socialLogin",
|
||||
);
|
||||
|
||||
const eoaWallets = sortWalletConfigs(
|
||||
nonLocalWalletConfigs.filter((w) => w.category !== "socialLogin"),
|
||||
);
|
||||
|
||||
const showNewToWallets =
|
||||
isCompact && (socialWallets.length === 0 || isWalletGroupExpanded);
|
||||
|
||||
// groups UI is showing a social login + grouping all eoa wallets together in a group
|
||||
// do this if there is social login and more than 2 eoa wallets
|
||||
const showGroupsUI =
|
||||
isCompact && socialWallets.length >= 1 && eoaWallets.length >= 2;
|
||||
|
||||
const showTOS = isCompact && (termsOfServiceUrl || privacyPolicyUrl);
|
||||
|
||||
const showFooter = Boolean(
|
||||
(!showGroupsUI && localWalletConfig) || showNewToWallets,
|
||||
);
|
||||
|
||||
const continueAsGuest = localWalletConfig && (
|
||||
<Button
|
||||
fullWidth
|
||||
variant={isCompact ? "outline" : "link"}
|
||||
style={
|
||||
!isCompact
|
||||
? {
|
||||
textAlign: "left",
|
||||
justifyContent: "flex-start",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onClick={() => {
|
||||
props.selectWallet(localWalletConfig);
|
||||
}}
|
||||
data-test="continue-as-guest-button"
|
||||
>
|
||||
Continue as Guest
|
||||
</Button>
|
||||
);
|
||||
|
||||
const twTitle = (
|
||||
<Container gap="xxs" center="y" flex="row">
|
||||
<TWIcon size={iconSize.md} />
|
||||
<ModalTitle> {props.title} </ModalTitle>
|
||||
</Container>
|
||||
);
|
||||
|
||||
const handleSelect = async (wallet: WalletConfig) => {
|
||||
if (connectionStatus !== "disconnected") {
|
||||
await disconnect();
|
||||
}
|
||||
props.selectWallet(wallet);
|
||||
};
|
||||
|
||||
const showSeperatorLine = showNewToWallets && !continueAsGuest && showTOS;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalTitle> {props.title} </ModalTitle>
|
||||
<Spacer y="xl" />
|
||||
|
||||
<WalletSelection
|
||||
walletConfigs={walletConfigs}
|
||||
selectWallet={props.selectWallet}
|
||||
/>
|
||||
|
||||
{localWalletInfo && (
|
||||
<>
|
||||
<Spacer y="xl" />
|
||||
<Flex justifyContent="center">
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => {
|
||||
props.selectWallet(localWalletInfo);
|
||||
}}
|
||||
data-test="continue-as-guest-button"
|
||||
>
|
||||
Continue as guest
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showGetStarted && (
|
||||
<>
|
||||
<Spacer y="xl" />
|
||||
<HelperLink
|
||||
as="button"
|
||||
onClick={props.onGetStarted}
|
||||
style={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
<Container scrollY flex="column" animate="fadein" fullHeight>
|
||||
{/* Header */}
|
||||
<Container p="lg">
|
||||
{isWalletGroupExpanded ? (
|
||||
<ModalHeader
|
||||
title={twTitle}
|
||||
onBack={() => {
|
||||
setIsWalletGroupExpanded(false);
|
||||
}}
|
||||
>
|
||||
Need help getting started?
|
||||
</HelperLink>
|
||||
</>
|
||||
/>
|
||||
) : (
|
||||
twTitle
|
||||
)}
|
||||
</Container>
|
||||
|
||||
{/* Body */}
|
||||
<Container
|
||||
expand
|
||||
scrollY
|
||||
px="md"
|
||||
style={{
|
||||
paddingTop: "2px",
|
||||
}}
|
||||
>
|
||||
{showGroupsUI ? (
|
||||
<>
|
||||
{isWalletGroupExpanded ? (
|
||||
<WalletSelection
|
||||
walletConfigs={eoaWallets}
|
||||
selectWallet={handleSelect}
|
||||
/>
|
||||
) : (
|
||||
<Container px="xs">
|
||||
<WalletSelection
|
||||
walletConfigs={socialWallets}
|
||||
selectWallet={handleSelect}
|
||||
/>
|
||||
|
||||
<TextDivider>
|
||||
<span> OR </span>
|
||||
</TextDivider>
|
||||
|
||||
<Spacer y="lg" />
|
||||
|
||||
{/* connect a wallet */}
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outline"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
gap: spacing.sm,
|
||||
padding: spacing.md,
|
||||
}}
|
||||
onClick={() => {
|
||||
setIsWalletGroupExpanded(true);
|
||||
}}
|
||||
>
|
||||
<Container flex="row" gap="xxs">
|
||||
{eoaWallets.slice(0, 2).map((w) => (
|
||||
<Img
|
||||
key={w.id}
|
||||
width={iconSize.sm}
|
||||
height={iconSize.sm}
|
||||
src={w.meta.iconURL}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
Connect a wallet
|
||||
</Button>
|
||||
|
||||
{continueAsGuest ? (
|
||||
<>
|
||||
<Spacer y="md" />
|
||||
{continueAsGuest}
|
||||
<Spacer y="xl" />
|
||||
</>
|
||||
) : (
|
||||
<Spacer y="xl" />
|
||||
)}
|
||||
</Container>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<WalletSelection
|
||||
walletConfigs={nonLocalWalletConfigs}
|
||||
selectWallet={handleSelect}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
{/* Footer */}
|
||||
{showFooter && (
|
||||
<ScreenBottomContainer>
|
||||
{showNewToWallets && (
|
||||
<Container
|
||||
flex="row"
|
||||
style={{
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Text color="secondaryText" size="sm" weight={500}>
|
||||
New to wallets?
|
||||
</Text>
|
||||
<Link
|
||||
weight={500}
|
||||
size="sm"
|
||||
target="_blank"
|
||||
href="https://ethereum.org/en/wallets/find-wallet/"
|
||||
>
|
||||
Get started
|
||||
</Link>
|
||||
</Container>
|
||||
)}
|
||||
|
||||
{!showGroupsUI && continueAsGuest}
|
||||
</ScreenBottomContainer>
|
||||
)}
|
||||
</>
|
||||
|
||||
{showTOS && !isWalletGroupExpanded && (
|
||||
<div>
|
||||
{showSeperatorLine && <Line />}
|
||||
|
||||
<Container
|
||||
p="md"
|
||||
style={
|
||||
!showSeperatorLine
|
||||
? {
|
||||
paddingTop: 0,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{isCompact && (
|
||||
<TOS
|
||||
termsOfServiceUrl={termsOfServiceUrl}
|
||||
privacyPolicyUrl={privacyPolicyUrl}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export const WalletSelection: React.FC<{
|
||||
walletConfigs: WalletConfig[];
|
||||
selectWallet: (wallet: WalletConfig) => void;
|
||||
maxHeight?: string;
|
||||
}> = (props) => {
|
||||
const modalConfig = useContext(ModalConfigCtx);
|
||||
const setModalConfig = useContext(SetModalConfigCtx);
|
||||
const walletConfigs = props.walletConfigs
|
||||
// show the installed wallets first
|
||||
.sort((a, b) => {
|
||||
const aInstalled = a.isInstalled ? a.isInstalled() : false;
|
||||
const bInstalled = b.isInstalled ? b.isInstalled() : false;
|
||||
|
||||
if (aInstalled && !bInstalled) {
|
||||
return -1;
|
||||
}
|
||||
if (!aInstalled && bInstalled) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
// show the wallets with selectUI first before others
|
||||
.sort((a, b) => {
|
||||
if (a.selectUI && !b.selectUI) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.selectUI && b.selectUI) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
const walletConfigs = sortWalletConfigs(props.walletConfigs);
|
||||
|
||||
return (
|
||||
<WalletListContainer>
|
||||
<WalletList>
|
||||
{walletConfigs.map((walletConfig) => {
|
||||
const isInstalled = walletConfig.isInstalled
|
||||
? walletConfig.isInstalled()
|
||||
: false;
|
||||
return (
|
||||
<li key={walletConfig.id}>
|
||||
<li key={walletConfig.id} data-full-width={!!walletConfig.selectUI}>
|
||||
{walletConfig.selectUI ? (
|
||||
<walletConfig.selectUI
|
||||
theme={modalConfig.theme}
|
||||
modalSize={modalConfig.modalSize}
|
||||
theme={
|
||||
typeof modalConfig.theme === "string"
|
||||
? modalConfig.theme
|
||||
: modalConfig.theme.type
|
||||
}
|
||||
supportedWallets={props.walletConfigs}
|
||||
onSelect={(data) => {
|
||||
props.selectWallet(walletConfig);
|
||||
@@ -131,65 +284,143 @@ export const WalletSelection: React.FC<{
|
||||
walletConfig={walletConfig}
|
||||
/>
|
||||
) : (
|
||||
<WalletButton
|
||||
type="button"
|
||||
onClick={() => {
|
||||
<WalletEntryButton
|
||||
walletConfig={walletConfig}
|
||||
selectWallet={() => {
|
||||
props.selectWallet(walletConfig);
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={walletConfig.meta.iconURL}
|
||||
width={iconSize.lg}
|
||||
height={iconSize.lg}
|
||||
loading="eager"
|
||||
/>
|
||||
<WalletName>{walletConfig.meta.name}</WalletName>
|
||||
{isInstalled && <InstallBadge> Installed </InstallBadge>}
|
||||
</WalletButton>
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</WalletListContainer>
|
||||
</WalletList>
|
||||
);
|
||||
};
|
||||
|
||||
const WalletListContainer = styled.ul`
|
||||
export function WalletEntryButton(props: {
|
||||
walletConfig: WalletConfig<any>;
|
||||
selectWallet: () => void;
|
||||
}) {
|
||||
const { walletConfig, selectWallet } = props;
|
||||
const isRecommended = walletConfig.recommended;
|
||||
return (
|
||||
<WalletButton
|
||||
type="button"
|
||||
onClick={() => {
|
||||
selectWallet();
|
||||
}}
|
||||
>
|
||||
<Img
|
||||
src={walletConfig.meta.iconURL}
|
||||
width={iconSize.xl}
|
||||
height={iconSize.xl}
|
||||
loading="eager"
|
||||
/>
|
||||
|
||||
<Container flex="column" gap="xxs" expand>
|
||||
<Text color="primaryText" weight={500}>
|
||||
{walletConfig.meta.name}
|
||||
</Text>
|
||||
|
||||
{isRecommended && (
|
||||
<Text size="sm" color="accentText" weight={500}>
|
||||
Recommended
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!isRecommended &&
|
||||
walletConfig.isInstalled &&
|
||||
walletConfig.isInstalled() && (
|
||||
<Text size="sm" weight={500}>
|
||||
Installed
|
||||
</Text>
|
||||
)}
|
||||
</Container>
|
||||
</WalletButton>
|
||||
);
|
||||
}
|
||||
|
||||
const WalletList = styled.ul<{ theme?: Theme }>`
|
||||
all: unset;
|
||||
list-style-type: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${spacing.xs};
|
||||
gap: 2px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
${noScrollBar}
|
||||
|
||||
/* to show the box-shadow of inputs that overflows */
|
||||
padding: 2px;
|
||||
margin: -2px;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: ${spacing.xl};
|
||||
`;
|
||||
|
||||
const WalletButton = styled.button<{ theme?: Theme }>`
|
||||
all: unset;
|
||||
padding: ${spacing.sm} ${spacing.md};
|
||||
border-radius: ${radius.sm};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${spacing.md};
|
||||
gap: ${spacing.sm};
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
color: ${(p) => p.theme.text.neutral};
|
||||
background: ${(p) => p.theme.bg.elevated};
|
||||
transition: 100ms ease;
|
||||
color: ${(p) => p.theme.colors.secondaryText};
|
||||
position: relative;
|
||||
border-radius: ${radius.md};
|
||||
padding: ${spacing.xs} ${spacing.xs};
|
||||
|
||||
&:hover {
|
||||
background: ${(p) => p.theme.bg.highlighted};
|
||||
background-color: ${(p) => p.theme.colors.walletSelectorButtonHoverBg};
|
||||
}
|
||||
|
||||
transition:
|
||||
background-color 200ms ease,
|
||||
transform 200ms ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.01);
|
||||
}
|
||||
`;
|
||||
|
||||
const InstallBadge = styled.span<{ theme?: Theme }>`
|
||||
padding: ${spacing.xxs} ${spacing.xs};
|
||||
font-size: ${fontSize.xs};
|
||||
background-color: ${(p) => p.theme.badge.secondary};
|
||||
border-radius: ${radius.lg};
|
||||
margin-left: auto;
|
||||
`;
|
||||
function sortWalletConfigs(walletConfigs: WalletConfig[]) {
|
||||
return (
|
||||
walletConfigs
|
||||
// show the installed wallets first
|
||||
.sort((a, b) => {
|
||||
const aInstalled = a.isInstalled ? a.isInstalled() : false;
|
||||
const bInstalled = b.isInstalled ? b.isInstalled() : false;
|
||||
|
||||
const WalletName = styled.span`
|
||||
font-size: ${fontSize.md};
|
||||
font-weight: 500;
|
||||
`;
|
||||
if (aInstalled && !bInstalled) {
|
||||
return -1;
|
||||
}
|
||||
if (!aInstalled && bInstalled) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
// show the reccomended wallets even before that
|
||||
.sort((a, b) => {
|
||||
if (a.recommended && !b.recommended) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.recommended && b.recommended) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
// show the wallets with selectUI first before others
|
||||
.sort((a, b) => {
|
||||
if (a.selectUI && !b.selectUI) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.selectUI && b.selectUI) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,3 +2,11 @@ export const reservedScreens = {
|
||||
main: "main",
|
||||
getStarted: "getStarted",
|
||||
};
|
||||
|
||||
export const modalMaxWidthCompact = "360px";
|
||||
export const modalMaxWidthWide = "730px";
|
||||
|
||||
export const defaultModalTitle = "Connect";
|
||||
|
||||
export const widemodalMaxHeight = "550px";
|
||||
export const compactmodalMaxHeight = "550px";
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export const Apple =
|
||||
"ipfs://Qmcyb1akWqyu8W4DgVWzZUozrCkfES8ow36jNhjs7arwvc/AppleGray.svg";
|
||||
|
||||
export const Chrome =
|
||||
"ipfs://QmPu4UKsektZixeZN4ixi8Ji9KbMuwcRKAp3JRK4jrcaMK/Chrome.svg";
|
||||
|
||||
export const GooglePlay =
|
||||
"ipfs://QmTSu87iXWpfcCgy15DX3Mgp1Rqb8zFAwXdbLNh6WxmpC4/GooglePlay.svg";
|
||||
@@ -0,0 +1,18 @@
|
||||
import { IconFC } from "./types";
|
||||
|
||||
export const AppleIcon: IconFC = (props) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={props.size}
|
||||
height={props.size}
|
||||
viewBox="0 0 23 28"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M22.4181 21.6041C22.008 22.5506 21.5224 23.422 20.9599 24.223C20.1931 25.3151 19.5652 26.0711 19.0814 26.4908C18.3313 27.1799 17.5277 27.5327 16.6671 27.5528C16.0493 27.5528 15.3043 27.3772 14.437 27.021C13.5669 26.6664 12.7673 26.4908 12.0362 26.4908C11.2694 26.4908 10.447 26.6664 9.56734 27.021C8.68636 27.3772 7.97665 27.5628 7.43403 27.5812C6.60879 27.6164 5.78624 27.2534 4.96519 26.4908C4.44116 26.0343 3.78569 25.2516 3.00047 24.1428C2.15799 22.9587 1.46536 21.5857 0.92274 20.0203C0.341611 18.3295 0.050293 16.6922 0.050293 15.1071C0.050293 13.2914 0.44307 11.7253 1.2298 10.413C1.84809 9.35886 2.67065 8.52734 3.70014 7.91691C4.72963 7.30648 5.84199 6.99541 7.03991 6.97551C7.69538 6.97551 8.55493 7.17804 9.6231 7.57607C10.6882 7.97544 11.3722 8.17797 11.672 8.17797C11.8962 8.17797 12.656 7.94116 13.944 7.46904C15.162 7.0312 16.19 6.84991 17.0321 6.92133C19.3141 7.10529 21.0285 8.00387 22.1687 9.62276C20.1278 10.858 19.1182 12.5881 19.1383 14.8076C19.1567 16.5363 19.7846 17.9749 21.0185 19.1172C21.5777 19.6473 22.2022 20.0571 22.897 20.3481C22.7463 20.7846 22.5872 21.2027 22.4181 21.6041ZM17.1845 1.03178C17.1845 2.38678 16.6889 3.65194 15.7011 4.82296C14.509 6.21507 13.0672 7.0195 11.5036 6.89256C11.4837 6.73 11.4721 6.55892 11.4721 6.37913C11.4721 5.07833 12.039 3.68622 13.0457 2.54798C13.5484 1.97167 14.1876 1.49247 14.9628 1.11021C15.7363 0.733653 16.4679 0.525407 17.156 0.489746C17.1761 0.670889 17.1845 0.85206 17.1845 1.03178Z"
|
||||
fill="#959CA0"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user