Compare commits

...
Author SHA1 Message Date
github-actions[bot]GitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
8a3993d42b Version Packages (#1613)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-09-18 12:13:49 -07:00
Manan TankandGitHub bab39980e7 [react] ConnectWallet minor UI fixes (#1621) 2023-09-18 18:56:07 +00:00
48295c0604 feat(sdk): centralize all blockchain reads via a single entrypoint (#1534)
Co-authored-by: Joaquim Verges <[email protected]>
2023-09-18 17:21:37 +00:00
YashandGitHub 3fd39cea0d Update addresses (#1617) 2023-09-15 20:26:16 +00:00
1fe867e205 [ReactNative] Connect Wallet V3 (#1592)
Co-authored-by: Manan Tank <[email protected]>
2023-09-15 17:47:40 +00:00
Manan TankandGitHub f5991387ef [react] ConnectWallet UI improvements + code cleanup (#1615) 2023-09-15 14:29:55 +00:00
3f3c63c01e [react] ConnectWallet v3 UI (#1550)
Co-authored-by: Joaquim Verges <[email protected]>
2023-09-15 11:21:03 +00:00
YashandGitHub f80cf0e17e [SDK] Remove ipfs dependency in tests (#1612) 2023-09-15 02:43:17 +00:00
Phillip HoandGitHub 10b7648857 chore: add evmMethod to usage traacker body (#1611) 2023-09-14 12:14:00 -07:00
260 changed files with 16784 additions and 4694 deletions
+7
View File
@@ -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 -1
View File
@@ -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": {
+6
View File
@@ -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
+2 -2
View File
@@ -14,8 +14,8 @@ export default {
],
"faucets": [],
"nativeCurrency": {
"name": "MAP",
"symbol": "MAP",
"name": "MAPO",
"symbol": "MAPO",
"decimals": 18
},
"infoURL": "https://maplabs.io",
+1 -1
View File
@@ -21,7 +21,7 @@ export default {
}
],
"infoURL": "https://manta-testnet.caldera.dev/",
"shortName": "manta",
"shortName": "mantaTestnet",
"chainId": 3441005,
"networkId": 3441005,
"icon": {
+1 -1
View File
@@ -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
+10
View File
@@ -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 -1
View File
@@ -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",
+12
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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",
+68
View File
@@ -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 -1
View File
@@ -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()}
@@ -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>
);
};
@@ -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>
);
}
@@ -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,
},
});
@@ -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>
);
};
@@ -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,
},
});
@@ -33,9 +33,7 @@ export const ConnectWalletHeader = ({
onPress={onBackPress}
color={theme.colors.iconPrimary}
/>
) : (
<View />
)}
) : null}
{walletLogoUrl ? (
<ImageSvgUri width={56} height={56} imageUrl={walletLogoUrl} />
) : null}
@@ -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"
@@ -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={
+14 -2
View File
@@ -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",
+21 -24
View File
@@ -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>;
};
@@ -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,
};
};
+136
View File
@@ -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
+3 -2
View File
@@ -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": {
+1 -1
View File
@@ -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);
`;
+1 -1
View File
@@ -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};
`;
+18 -10
View File
@@ -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 -10
View File
@@ -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;
+7 -4
View File
@@ -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}
/>
);
+38 -39
View File
@@ -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};
`;
+134
View File
@@ -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};
}
`;
+2 -1
View File
@@ -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);
`;
+17 -13
View File
@@ -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};
`;
+270 -43
View File
@@ -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>
);
}
+4 -9
View File
@@ -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};
`;
+2 -2
View File
@@ -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>
-48
View File
@@ -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 {
+3 -3
View File
@@ -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};
`;
+175 -19
View File
@@ -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}
}
`;
+38 -60
View File
@@ -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;
+33 -31
View File
@@ -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;
}
`;
+39 -12
View File
@@ -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;
}
`;
+116 -102
View File
@@ -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);
},
+1 -1
View File
@@ -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 (
+5 -2
View File
@@ -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";
+2
View File
@@ -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