Author SHA1 Message Date
Jakub Kotula 5ae975fbfc f 2024-11-19 22:48:55 +01:00
marcelines 4f21bb87c6 Update copy from Pro to Shell (#21) 2024-11-18 13:10:35 +00:00
marcelines 37f474cdfd Unifying dialogs (#19) 2024-11-18 08:51:28 +00:00
Jakub Kotula 30a684e164 f links 2024-11-15 16:54:51 +01:00
Jakub Kotula 270224fb77 f docs&blog 2024-11-15 16:17:54 +01:00
marcelinesandFelicio Mununga 29ec47908b Shopify Buy keycard dialog (#15)
Co-authored-by: Felicio Mununga <felicio@users.noreply.github.com>
2024-11-14 16:38:54 +00:00
Jakub Kotula 5328aef1fb Get notified (#14) 2024-11-14 15:45:54 +01:00
marcelines bc63b4d9d1 Responsive view (#12) 2024-11-14 14:38:34 +00:00
Felicio Mununga 76ca4ca576 update Next (#10) 2024-11-12 12:28:14 +01:00
Jakub Kotula 264fafe1de legal pages (#13) 2024-11-08 09:52:34 +01:00
Jakub Kotula 8ff442a611 blog (#11) 2024-11-08 09:32:43 +01:00
Jakub Kotula 1f5e69f31c docs (#7) 2024-11-08 09:14:10 +01:00
Felicio Mununga 830afae870 add Shopify API (#9) 2024-11-07 13:19:34 +01:00
Felicio Mununga f1703349c0 use Shopify UTM params (#8) 2024-11-05 10:47:32 +01:00
marcelines dd75005415 Keycard pro page (#6) 2024-11-04 19:03:08 +00:00
marcelines d299aa08d5 Keycard page (#3) 2024-11-04 11:24:22 +00:00
marcelinesandFelicio Mununga c960f368cb Homepage (#1)
Co-authored-by: Felicio Mununga <felicio@users.noreply.github.com>
2024-10-29 11:42:54 +00:00
225 changed files with 4524 additions and 4655 deletions
+1 -1
View File
@@ -1 +1 @@
pnpm dlx lint-staged
pnpx lint-staged
-1
View File
@@ -1 +0,0 @@
20.18.0
-7
View File
@@ -1,7 +0,0 @@
; https://pnpm.io/npmrc#node-options
; why: https://github.com/vercel/next.js/discussions/70423
node-options=--network-family-autoselection-attempt-timeout=1000
; https://pnpm.io/npmrc#node-version
node-version=20.18.0
; https://pnpm.io/npmrc#engine-strict
engine-strict=true
+1 -1
View File
@@ -25,7 +25,7 @@
// note: config https://github.com/microsoft/vscode-js-debug/blob/e620efe1841742402a332b532df2aa2ec95361f6/OPTIONS.md
"type": "node-terminal",
"request": "launch",
"command": "NODE_OPTIONS='' pnpm dev --port 3030",
"command": "yarn dev -- --port 3030",
"serverReadyAction": {
"pattern": "\\s- Local:\\s+(https?://\\S+)",
"action": "startDebugging",
+2 -4
View File
@@ -5,11 +5,9 @@ title: Card Setup
# Preparing the card
Keycard ships with all applets fully installed but not initialized. The most convenient way to initialize and manage the card is with [Status apps](https://status.app/apps) (mobile and desktop).
Keycard ships with all applets fully installed but not initialized. The most convenient way to initialize and manage the card is to install the [Keycard Connect](https://github.com/status-im/keycard-connect) Android app. It allows to fully initialize a card with pairing key, PIN, PUK and BIP39 seed. It also provides basic functionality like changing credentials, changing or removing keys and clear pairing slots. The last functionality is especially useful if you are integrating with Keycard and accidentally fill all pairing slots. Using the "Unpair others" button will remove all pairing except the Keycard Connect's one.
Alternatively, install the [Keycard Connect](https://github.com/keycard-tech/keycard-connect) Android app. It allows to fully initialize a card with pairing key, PIN, PUK and BIP39 seed. It also provides basic functionality like changing credentials, changing or removing keys and clear pairing slots. The last functionality is especially useful if you are integrating with Keycard and accidentally fill all pairing slots. Using the "Unpair others" button will remove all pairing except the Keycard Connect's one.
In some cases you might want to reset the applet status completely for testing purposes. This is done by reinstalling the applet, which can be accomplished using the [Keycard CLI](https://github.com/keycard-tech/keycard-cli). To use the Keycard CLI, a USB reader is needed to be able to communicate with the card. The repository contains example scripts for applet reinstallation.
In some cases you might want to reset the applet status completely for testing purposes. This is done by reinstalling the applet, which can be accomplished using the [Keycard CLI](https://github.com/status-im/keycard-cli). To use the Keycard CLI, a USB reader is needed to be able to communicate with the card. The repository contains example scripts for applet reinstallation.
## Card requirements
+3 -2
View File
@@ -30,5 +30,6 @@ The current version of the applet is 3.1. This documentation applies to said ver
You are at the right place to find informations about Keycard and get you going.
If you need more and want to discuss:
- to check or raise issues/bugs/feature requests please do so in our [GitHub repos](resources)
- get in touch with the team by [email](mailto:support@keycard.tech)
- for live discussion and help, go to [Keycard Status channel](https://join.status.im/chat/public/status-keycard)
- to check or raise issues/bugs/feature requests please do so in our [GitHub repos](https://keycard.tech/docs/resources.html)
- check our latest Keycard news on [Discuss](https://discuss.status.im/c/keycard)
+9 -9
View File
@@ -9,15 +9,15 @@ Here you find the links to all repositories and tools related to Keycard develop
## GitHub repositories
* [Javacard applet](https://github.com/keycard-tech/status-keycard)
* [Java SDK](https://github.com/keycard-tech/status-keycard-java)
* [Go SDK](https://github.com/keycard-tech/keycard-go/)
* [Swift SDK](https://github.com/keycard-tech/Keycard.swift)
* [Keycard on Ledger](https://github.com/keycard-tech/keycard-ledger)
* [React Native library](https://github.com/keycard-tech/react-native-status-keycard)
* [Go Status library](https://github.com/keycard-tech/status-keycard-go)
* [Javacard applet](https://github.com/status-im/status-keycard)
* [Java SDK](https://github.com/status-im/status-keycard-java)
* [Go SDK](https://github.com/status-im/keycard-go/)
* [Swift SDK](https://github.com/status-im/Keycard.swift)
* [Keycard on Ledger](https://github.com/status-im/keycard-ledger)
* [React Native library](https://github.com/status-im/react-native-status-keycard)
* [Go Status library](https://github.com/status-im/status-keycard-go)
## Tools
* [Keycard CLI](https://github.com/keycard-tech/keycard-cli)
* [Keycard Desktop](https://github.com/choppu/keycard-desktop)
* [Keycard CLI](https://github.com/status-im/keycard-cli)
* [Keycard Desktop](https://github.com/choppu/keycard-desktop)
+3 -3
View File
@@ -33,10 +33,10 @@ When however the data to be signed contains `uint256 amount` and `address curren
![EIP-712 ERC20 screen](/assets/docs/web3-erc20.png)
If the currency field address is considered to be the address of an ERC20 token. If this address is zero, the amount is interpreted as plain ETH. This transaction format is used by the [Keycard Payment Network](https://github.com/keycard-tech/payment-network-apps) project.
If the currency field address is considered to be the address of an ERC20 token. If this address is zero, the amount is interpreted as plain ETH. This transaction format is used by the [Keycard Payment Network](https://github.com/status-im/payment-network-apps) project.
Another example of a custom screen, is the one displayed for the [Keycard Redeem](https://github.com/keycard-tech/keycard-redeem) project. In this case we recognize the presence of the fields `address recipient` and `bytes32 code` field as a redeem request and we display the recipient field address clearly in the signature screen. If the recipient field in account managed by Status, instead of the plain address, the account name is shown. See screenshot below
Another example of a custom screen, is the one displayed for the [Keycard Redeem](https://github.com/status-im/keycard-redeem) project. In this case we recognize the presence of the fields `address recipient` and `bytes32 code` field as a redeem request and we display the recipient field address clearly in the signature screen. If the recipient field in account managed by Status, instead of the plain address, the account name is shown. See screenshot below
![EIP-712 ERC20 screen](/assets/docs/web3-redeem.png)
One thing to keep in mind however, is that these custom screens are only there to provide better UX for some specific use case we are aware of. Any unrecognized message format can still be displayed generically and signed. If you are implementing a web3 browser you can adopt any strategy you prefer on this. Conversely, if you are developing SmartContract and dApps, you might want consider naming the fields of the message in a way that will trigger a specific interface on a browser. For example if your use case scenario involve transferring a certain `amount` of `currency` in a transaction, naming the message fields like that will show an easy to understand screen in Status.
One thing to keep in mind however, is that these custom screens are only there to provide better UX for some specific use case we are aware of. Any unrecognized message format can still be displayed generically and signed. If you are implementing a web3 browser you can adopt any strategy you prefer on this. Conversely, if you are developing SmartContract and dApps, you might want consider naming the fields of the message in a way that will trigger a specific interface on a browser. For example if your use case scenario involve transferring a certain `amount` of `currency` in a transaction, naming the message fields like that will show an easy to understand screen in Status.
+1 -1
View File
@@ -1,5 +1,5 @@
---
title: Keycard - Privacy Policy
title: Keycard Presale Website - Privacy Policy
---
This Privacy Policy is intended to inform users of our approach to privacy in respect of this website (“**Website**”). In this regard, if you are visiting our Website, this Privacy Policy applies to you.
+1 -1
View File
@@ -1,5 +1,5 @@
---
title: Keycard - Terms of Use
title: Keycard Presale - Terms of Use
---
These website terms of use (“**Website Terms of Use**”) are entered into by you and us, and they govern your access and use of this Website, including any content and functionality contained in the Website.
Vendored
-17
View File
@@ -1,17 +0,0 @@
import type * as React from 'react'
// why: https://github.com/mdx-js/mdx/issues/2487#issuecomment-2407661869
// why: https://react.dev/blog/2024/04/25/react-19-upgrade-guide#the-jsx-namespace-in-typescript
declare global {
namespace JSX {
// type ElementClass = React.JSX.ElementClass
// type Element = React.JSX.Element
type IntrinsicElements = React.JSX.IntrinsicElements
}
}
declare module 'framer-motion' {
export interface MotionProps {
className?: string
}
}
-5
View File
@@ -7,11 +7,6 @@ const config = {
hostname: 'demo.ghost.io',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'news.keycard.tech',
pathname: '/**',
},
],
},
transpilePackages: [
+6 -29
View File
@@ -16,19 +16,11 @@
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.3",
"@react-three/drei": "^9.117.2",
"@react-three/fiber": "9.0.0-rc.1",
"@react-three/postprocessing": "^2.16.3",
"@status-im/icons": "^1.0.2",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^5.59.17",
"@tryghost/content-api": "^1.11.21",
"@types/three": "^0.170.0",
"@types/unist": "^3.0.3",
"@vercel/analytics": "^1.4.1",
"cva": "1.0.0-beta.1",
"d3-time-format": "^4.1.0",
"embla-carousel-react": "^8.3.1",
@@ -37,16 +29,13 @@
"github-slugger": "^2.0.0",
"google-auth-library": "^9.14.2",
"google-spreadsheet": "^4.1.4",
"next": "15.0.3",
"next": "15.0.2",
"next-mdx-remote": "^5.0.0",
"react": "19.0.0-rc.1",
"react": "^18.3.1",
"react-children-utilities": "^2.10.0",
"react-dom": "19.0.0-rc.1",
"react-error-boundary": "^5.0.0",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.1",
"server-only": "^0.0.1",
"three": "^0.170.0",
"three-stdlib": "^2.34.0",
"ts-pattern": "^5.5.0",
"zod": "^3.23.8"
},
@@ -55,13 +44,13 @@
"@types/d3-time-format": "^4.0.3",
"@types/mdx": "^2.0.13",
"@types/node": "^20",
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@types/tryghost__content-api": "^1.3.17",
"@types/unist": "^3.0.3",
"embla-carousel": "^8.3.1",
"eslint": "^8.57.1",
"eslint-config-next": "15.0.3",
"eslint-config-next": "15.0.2",
"eslint-plugin-tailwindcss": "^3.17.5",
"husky": "^9.1.6",
"lint-staged": "^15.2.10",
@@ -81,9 +70,6 @@
"unified": "^10.1.2",
"unist-util-visit": "^5.0.0"
},
"engines": {
"node": "20.x"
},
"lint-staged": {
"*.{js,mjs,ts,tsx}": [
"eslint --fix",
@@ -95,14 +81,5 @@
"*.{md,mdx}": [
"prettier --write"
]
},
"pnpm": {
"overrides": {
"@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
},
"patchedDependencies": {
"@react-three/postprocessing": "patches/@react-three__postprocessing.patch"
}
}
}
@@ -1,16 +0,0 @@
diff --git a/dist/EffectComposer.js b/dist/EffectComposer.js
index 335539d470b862efff4d34818a28d1ed78255005..c4e47fad5ea95dd28ef1570919bbcba0b1883ad8 100644
--- a/dist/EffectComposer.js
+++ b/dist/EffectComposer.js
@@ -77,9 +77,9 @@ const EffectComposer = React.memo(
useLayoutEffect(() => {
const passes = [];
if (group.current && instance.current && composer) {
- const children2 = instance.current.objects;
+ const children2 = instance.current.children;
for (let i = 0; i < children2.length; i++) {
- const child = children2[i];
+ const child = children2[i].object;
if (child instanceof Effect) {
const effects = [child];
if (!isConvolution(child)) {
+1187 -1764
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

@@ -0,0 +1,115 @@
import { Button } from '~components/button'
import { ButtonLink } from '~components/button-link'
import { BuyKeycardDialog } from '~components/buy-keycard-dialog'
import { GetNotifiedDialog } from '~components/get-notified-dialog'
import { cx } from 'cva'
import Image from 'next/image'
import React from 'react'
type Props = {
image: string
imageClassName?: string
name: string
secondName?: string
title: React.ReactNode
description: string
buttons: React.ReactNode
info: string
}
const Section = (props: Props) => {
const {
image,
name,
title,
description,
buttons,
info,
imageClassName,
secondName,
} = props
return (
<div className="relative w-full overflow-clip">
<div className="absolute inset-0 z-10 h-1/2 w-full bg-gradient-to-b from-dark-100 to-[transparent]" />
<Image
src={image}
alt={name}
width={900}
height={600}
draggable={false}
className={cx([
'pointer-events-none absolute left-1/2 z-20 max-w-[549px] -translate-x-1/2 select-none lg:top-6',
imageClassName,
])}
/>
<div className="flex size-full flex-col items-center justify-end rounded-28 border border-white-8 bg-white-3 px-5 pb-20 pt-[300px] lg:px-10 lg:pb-[120px] xl:px-[98px]">
<div className="container relative z-10 flex max-w-[434px] flex-col items-center justify-center px-4">
<p className="pb-2 text-24 font-600 text-white-95">
{name} <span className="font-200">{secondName}</span>
</p>
<h1 className="flex pb-4 text-center font-lora text-32 font-400 lg:text-48">
{title}
</h1>
<p className="pb-8 text-center text-20 font-300 text-white-80">
{description}
</p>
<div className="flex space-x-4">{buttons}</div>
<p className="pt-6 text-16 font-300 text-white-60">{info}</p>
</div>
</div>
</div>
)
}
const BuyCards = () => {
return (
<section className="grid grid-cols-1 gap-10 overflow-clip pb-2 pt-[160px] full-view-port lg:grid-cols-2 lg:flex-row lg:gap-2 lg:pt-[200px] lg:remove-full-view-port">
<Section
image="/assets/bottom-keycard.png"
name="keycard"
title={
<>
A card shaped <br /> hardware wallet
</>
}
description="Something will say here about this product. Certainly, you don't want to miss it."
buttons={
<>
<BuyKeycardDialog>
<Button variant="primary">Buy Keycard</Button>
</BuyKeycardDialog>
<ButtonLink href="/keycard" variant="secondary">
Learn more
</ButtonLink>
</>
}
info="Starts from $25"
/>
<Section
image="/assets/bottom-keycard-shell.png"
imageClassName="pt-10 lg:pt-0 lg:-mb-20 lg:w-full"
name="keycard"
secondName="shell"
title={
<>
One device for all <br /> your keycards
</>
}
description="Something will say here about this product. Certainly you don't want to miss it"
buttons={
<>
<GetNotifiedDialog>
<Button>Get notified</Button>
</GetNotifiedDialog>
<ButtonLink href="/keycard-shell" variant="secondary">
Learn more
</ButtonLink>
</>
}
info="Coming 2025"
/>
</section>
)
}
export { BuyCards }
@@ -0,0 +1,422 @@
import { Tag } from '~components/tag'
import { Tooltip } from '~components/tooltip'
import { Check, Info, Minus, Shield } from '~icons'
import { createElement } from 'react'
const features: Array<Feature> = [
{
name: 'Competitive pricing',
tooltip: 'Pricing information',
keycardShell: {
label: 'TBD',
},
keycard: {
label: '$25',
},
tangem: {
label: '$55',
},
ledger: {
label: '$80',
},
},
{
name: 'EVM chain compatible',
keycardShell: {
featured: true,
},
keycard: {
featured: true,
},
tangem: {
featured: true,
},
ledger: {
featured: true,
},
},
{
name: 'Bitcoin compatible',
keycardShell: {
featured: true,
},
keycard: {
featured: true,
tooltip: 'Bitcoin only',
},
tangem: {
featured: true,
},
ledger: {
featured: true,
},
},
{
name: 'Hardware level protection',
tooltip: 'Security certification level',
keycardShell: {
badge: {
text: 'EAL 6+',
gradient: true,
icon: Shield,
},
},
keycard: { badge: { text: 'EAL 6+', gradient: true, icon: Shield } },
tangem: { badge: { text: 'EAL 6+', gradient: true, icon: Shield } },
ledger: { badge: { text: 'EAL 5+' } },
},
{
name: 'Mobile friendly',
tooltip: 'Compatible with mobile devices',
keycardShell: {
featured: true,
tooltip: 'iOS and Android',
},
keycard: {
featured: true,
},
tangem: {
featured: true,
},
ledger: {
featured: false,
},
},
{
name: 'Unlimited master keys',
keycardShell: {
featured: true,
},
keycard: {
featured: true,
tooltip: 'No limit on master keys',
},
tangem: {
featured: true,
tooltip: 'No limit on master keys',
},
ledger: {
featured: false,
},
tooltip: 'No limit on master keys',
},
{
name: 'Easy back up of master keys',
keycardShell: {
featured: true,
},
keycard: {
featured: true,
},
tangem: {
featured: true,
},
ledger: {
featured: false,
},
},
{
name: 'Open ecosystem',
tooltip: 'Part of an open ecosystem',
keycardShell: {
featured: true,
},
keycard: {
featured: true,
},
tangem: {
featured: false,
},
ledger: {
featured: true,
},
},
{
name: 'Open source secure element',
keycardShell: {
featured: true,
tooltip: 'Secure element is open source',
},
keycard: {
featured: false,
},
tangem: {
featured: false,
},
ledger: {
featured: false,
},
},
{
name: "Secure element can't be upgraded",
keycardShell: {
featured: true,
},
keycard: {
featured: false,
},
tangem: {
featured: false,
tooltip: 'Not applicable',
},
ledger: {
featured: false,
tooltip: 'Not applicable',
},
},
{
name: 'QR transaction signing with any wallet',
tooltip: 'Sign transactions using QR codes',
keycardShell: {
featured: true,
},
keycard: {
featured: true,
tooltip: 'Only with Keycard wallet',
},
tangem: {
featured: false,
},
ledger: {
featured: false,
},
},
{
name: 'Airgap capable',
tooltip: 'Can operate without direct connection',
keycardShell: {
featured: true,
},
keycard: {
featured: false,
},
tangem: {
featured: false,
},
ledger: {
featured: false,
},
},
{
name: 'Modular architecture',
keycardShell: {
featured: true,
},
keycard: {
featured: false,
},
tangem: {
featured: false,
},
ledger: {
featured: false,
},
},
{
name: 'Secure element has its own usages',
keycardShell: {
featured: true,
tooltip: 'Multiple use cases for secure element',
},
keycard: {
featured: 'n/a',
},
tangem: {
featured: 'n/a',
},
ledger: {
featured: false,
},
},
{
name: 'Can be used with future signing schemes',
tooltip: 'Future-proof design',
keycardShell: {
featured: true,
},
keycard: {
featured: 'n/a',
},
tangem: {
featured: 'n/a',
},
ledger: {
featured: false,
},
},
{
name: 'Removable battery',
tooltip: 'Battery can be replaced',
keycardShell: {
featured: true,
},
keycard: {
featured: 'n/a',
tooltip: 'Not applicable',
},
tangem: {
featured: 'n/a',
tooltip: 'Not applicable',
},
ledger: {
featured: false,
},
},
{
name: 'USBC + Camera',
keycardShell: {
featured: true,
},
keycard: {
featured: 'n/a',
},
tangem: {
featured: 'n/a',
},
ledger: {
featured: false,
},
},
{
name: 'Oshwa label',
keycardShell: {
featured: true,
},
keycard: {
featured: false,
},
tangem: {
featured: false,
},
ledger: {
featured: false,
},
},
] as const
type FeatureVariant = {
label?: string
badge?: {
text: string
icon?: React.FC
gradient?: boolean
}
featured?: boolean | 'n/a'
tooltip?: string
}
type Feature = {
name: string
tooltip?: string
keycardShell: FeatureVariant
keycard: FeatureVariant
tangem: FeatureVariant
ledger: FeatureVariant
}
const FeatureInfo = ({
variant,
}: {
variant: FeatureVariant | undefined | string
}) => {
if (!variant || typeof variant === 'string') return null
if (variant.featured === true) return <Check className="text-orange" />
if (variant.featured === false)
return <Minus className="size-4 text-white-60" />
if (variant.featured === 'n/a') return <p className="text-white-60">n/a</p>
if (variant.label) return <p className="text-white-95">{variant.label}</p>
if (variant.badge) {
return (
<Tag
gradient={variant.badge.gradient}
icon={variant.badge.icon && createElement(variant.badge.icon)}
size="small"
>
{variant.badge.text}
</Tag>
)
}
}
const TooltipInfo = ({
variant,
}: {
variant: FeatureVariant | undefined | string
}) => {
if (typeof variant === 'string') return null
if (!variant || !variant.tooltip) return null
return (
<Tooltip label={variant.tooltip || ''}>
<div className="absolute left-[calc(50%+10px)] flex">
<Info className="ml-2 flex-shrink-0 text-white-40 transition-colors hover:text-white-60" />
</div>
</Tooltip>
)
}
const ComparisonTable = () => {
const products: Array<keyof Feature> = [
'keycardShell',
'keycard',
'tangem',
'ledger',
]
return (
<section className="mx-auto max-w-[1352px] overflow-x-hidden pt-[120px] text-white-95 lg:pt-[200px]">
<h1 className="px-3 font-lora text-32 text-white-95 min-[1512px]:px-0">
Going one step further.
</h1>
<div className="overflow-x-auto px-3 scrollbar-none min-[1512px]:px-0">
<div className="w-[684px] pt-14 md:w-full lg:w-full lg:pt-20">
<div className="grid grid-cols-[1.33fr,1fr,1fr,1fr,1fr] justify-items-center text-center font-lora text-24 font-400 text-white-90 lg:grid-cols-[2fr,1fr,1fr,1fr,1fr] lg:text-24">
<div className="justify-self-start p-4 pb-5 pl-6">Feature</div>
<div className="p-4 pb-5">Shell</div>
<div className="p-4 pb-5">Keycard</div>
<div className="p-4 pb-5">Tangem</div>
<div className="p-4 pb-5">Ledger</div>
</div>
<div className="rounded-28 border border-white-12 bg-white-3 px-6">
{features.map((feature, index) => {
return (
<div
key={index}
className="grid grid-cols-[1.33fr,1fr,1fr,1fr,1fr] border-b border-dashed border-white-12 font-300 first:pt-2 last:border-b-0 last:pb-2 lg:grid-cols-[2fr,1fr,1fr,1fr,1fr]"
>
<div className="w-full p-5 first:pl-0 lg:flex lg:items-center">
<span className="relative inline whitespace-normal">
{feature.name.split(' ').slice(0, -1).join(' ')}{' '}
<span className="inline-flex items-center">
{feature.name.split(' ').slice(-1)[0]}
{feature.tooltip && (
<Tooltip label={feature.tooltip}>
<Info className="ml-2 flex-shrink-0 text-white-40 transition-colors hover:text-white-60" />
</Tooltip>
)}
</span>
</span>
</div>
{products.map(product => (
<div
key={product}
className="relative flex items-center justify-center last:pr-0"
>
<FeatureInfo variant={feature[product]} />
<TooltipInfo variant={feature[product]} />
</div>
))}
</div>
)
})}
</div>
</div>
</div>
</section>
)
}
export { ComparisonTable }
@@ -1,10 +1,8 @@
import * as Accordion from '@radix-ui/react-accordion'
import { ChevronDownIcon } from '@status-im/icons/20'
import { TwitterIcon } from '@status-im/icons/social'
import { ButtonLink } from '~components/button-link'
import { Image } from '~components/image'
import { Link } from '~components/link'
import { ChevronDown, Discord, X } from '~icons'
import { cx } from 'cva'
import Image from 'next/image'
const faqs = [
{
@@ -14,19 +12,8 @@ const faqs = [
},
{
question: 'Which assets does Keycard support?',
answer: (
<>
Keycard works seamlessly with Ethereum and EVM based assets. It supports
all assets on{' '}
<Link
href="https://tokenlists.org/token-list?url=https://ipfs.io/ipns/tokens.uniswap.org"
className="underline decoration-1 underline-offset-2 transition-colors hover:text-orange"
>
Uniswap Labs default token list
</Link>
.
</>
),
answer:
'Keycard supports a wide range of cryptocurrencies including Bitcoin, Ethereum, and many ERC-20 tokens.',
},
{
question: 'Are my funds safe if I lose my Keycard?',
@@ -39,28 +26,28 @@ const faqs = [
'Keycard is compatible with various software wallets. Please check our documentation for a full list.',
},
{
question: 'Why should I get a Keycard?',
answer:
'Keycard is a secure, pocket-sized hardware wallet with a provably genuine, immutable chip to protect your digital assets.',
question: 'What happens if I lose access to my Keycard?',
answer: 'You can recover your assets using your backup seed phrase.',
},
]
const Faqs = () => {
return (
<section className="mx-auto max-w-[1352px] pt-[144px]">
<section className="mx-auto max-w-[1352px]">
<div className="flex flex-col items-start justify-between px-3 lg:flex-row lg:items-end min-[1512px]:px-0">
<div className="flex w-full flex-1 flex-col lg:w-auto">
<div className="flex justify-center pt-[120px] lg:hidden">
<div className="flex w-full flex-col lg:pt-[200px]">
<div className="block pt-[120px] lg:hidden">
<Image
src="/assets/faqs-v1.png"
src="/assets/faqs-mobile.png"
alt="FAQ Background"
width={264}
height={488}
width={736}
height={736}
className="w-full"
/>
</div>
<h2 className="mt-14 font-lora text-32 text-white-95 lg:mt-0">
<h1 className="font-lora text-32 text-white-95">
Frequently asked questions
</h2>
</h1>
<Accordion.Root
className="flex-1 pt-8 lg:max-w-[549px] lg:pt-14"
type="single"
@@ -83,55 +70,61 @@ const Faqs = () => {
>
{faq.question}
<div className="rounded-[10px] border border-white-12 bg-white-8 p-[6px]">
<ChevronDownIcon className="text-white-95 transition-transform duration-300 ease-[cubic-bezier(0.87,_0,_0.13,_1)] group-data-[state=open]:rotate-180" />
<ChevronDown className="text-white-95 transition-transform duration-300 ease-[cubic-bezier(0.87,_0,_0.13,_1)] group-data-[state=open]:rotate-180" />
</div>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content
className={cx(
'overflow-hidden pr-14 text-16 font-300 text-white-80 data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown',
'overflow-hidden text-16 font-300 text-white-80 data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown',
)}
>
<div className="pt-4">{faq.answer}</div>
<span className="flex pt-4">{faq.answer}</span>
</Accordion.Content>
</Accordion.Item>
))}
</Accordion.Root>
</div>
<div className="flex w-full flex-col lg:w-auto lg:items-center">
<div className="relative hidden lg:block">
<div className="flex flex-col items-end">
<div className="relative -right-1/4 -mt-48 hidden translate-x-[156px] translate-y-10 lg:block">
<div className="absolute top-32 z-10 h-1/2 w-full bg-gradient-to-t from-[transparent] to-dark-100" />
<Image
src="/assets/faqs-v1.png"
src="/assets/faqs.png"
alt="FAQ Background"
height={488}
width={264}
className="relative"
width={736}
height={736}
/>
</div>
<div className="mt-14 flex flex-1 flex-col gap-6 rounded-28 border border-white-8 bg-white-4 p-6 pt-5 lg:mt-10 lg:max-w-[320px]">
<div className="mt-14 flex w-full flex-col gap-6 rounded-28 border border-white-8 bg-white-3 p-6 pt-5 lg:mt-0 lg:max-w-[320px]">
<div className="flex flex-col gap-[6px]">
<p className="font-lora text-24 font-400 text-white-95">
Still have questions?
</p>
<p className="text-16 font-300 text-white-80">
Reach out to our team or engage with us on X.
Reach out to our team or engage with our community on Discord or
X.
</p>
</div>
<div className="flex gap-3">
<ButtonLink
href="mailto:support@keycard.tech"
variant="secondary"
>
<ButtonLink href="/" variant="secondary">
Get in touch
</ButtonLink>
<ButtonLink
href="https://discord.com" // TODO: fix this link
variant="secondary"
className="px-[9px]"
aria-label="Keycard on Discord"
>
<Discord />
</ButtonLink>
<ButtonLink
href="https://x.com/keycard_"
variant="secondary"
className="px-[9px]"
aria-label="Keycard on X"
>
<TwitterIcon />
<X />
</ButtonLink>
</div>
</div>
+51
View File
@@ -0,0 +1,51 @@
import { Button } from '~components/button'
import { ButtonLink } from '~components/button-link'
import { BuyKeycardDialog } from '~components/buy-keycard-dialog'
import Image from 'next/image'
const Hero = () => {
return (
<section className="relative flex h-[calc(100svh-80px)] flex-col justify-end overflow-clip rounded-t-28 border border-white-8 bg-white-3 px-0 pt-0 full-view-port lg:h-auto lg:px-[72px] lg:py-20 lg:remove-full-view-port">
<Image
alt="Keycard"
src="/assets/hero-bg.png"
width={1600}
height={1600}
className="absolute bottom-0 right-0 z-0 hidden w-auto bg-cover bg-center lg:block"
priority
/>
<Image
alt="Keycard"
src="/assets/hero-bg-mobile.png"
width={1600}
height={1600}
className="absolute right-0 top-0 block w-full bg-cover bg-center lg:hidden"
priority
/>
<div className="relative z-10 flex max-w-[434px] flex-col px-5 lg:pt-0">
<p className="pb-2 text-24 font-600 text-white-95">keycard</p>
<h1 className="flex pb-8 font-lora text-32 font-400 lg:pb-4 lg:text-48">
A card shaped <br /> hardware wallet
</h1>
<p className="pb-8 text-20 font-300 text-white-80">
Store and trade your crypto with a simple, secure and slim hardware
wallet.
</p>
<div className="flex space-x-4">
<BuyKeycardDialog>
<Button variant="primary">Buy Keycard</Button>
</BuyKeycardDialog>
<ButtonLink href="/keycard" variant="secondary">
Learn more
</ButtonLink>
</div>
<p className="pb-8 pt-6 text-16 font-300 text-white-60 lg:pb-0">
From $25
</p>
</div>
</section>
)
}
export { Hero }
@@ -0,0 +1,138 @@
import { ButtonLink } from '~components/button-link'
import { Tag } from '~components/tag'
import { Github, Infinity, Shield } from '~icons'
import { cx } from 'cva'
import Image from 'next/image'
import { createElement } from 'react'
const features = [
{
title: 'Best in class security',
description:
'Our secure element has the highest level of security EAL6+ certified by Common Criteria.',
image: '/assets/feature-keycard.png',
imageClassName: '!justify-start',
badge: { icon: Shield, text: 'EAL 6+', gradient: true },
className: 'row-span-2 col-span-1',
},
{
title: '100% open source',
description:
'We have nothing to hide! Our software, hardware and construction is fully open source.',
button: { icon: Github, text: 'View on GitHub' },
className: 'col-span-1',
},
{
title: 'Fully airgapped',
description:
"Through Keycard Shell's camera or Keycard's contactless nature, our products are truly airgapped.",
image: '/assets/feature-keycard-shell.png',
badge: { text: '0 cables' },
className: 'row-span-2 col-span-1 flex-col-reverse',
},
{
title: 'Made to last',
description:
'Your keycard has a life expectancy of 25+ years, resists water and dust. It will still securely store your keys.',
badge: { text: '25+ years' },
className: 'col-span-1',
},
{
title: 'Easy to backup',
description:
'Create cards to back up your master key and store them in a safe place instead of the typical piece of paper.',
badge: { icon: Infinity, text: 'backups' },
className: 'col-span-1',
},
{
title: 'Discreet',
description:
'With its light, small and discreet form factor your Keycard can go unnoticed in your wallet.',
badge: { text: 'Credit card format' },
className: 'col-span-1',
},
]
const UnderlinedWord = ({ children }: { children: React.ReactNode }) => (
<div className="relative inline-block">
<span className="relative z-10 [text-shadow:_-3px_2px_black,_4px_1px_black,_2px_0px_black,_3px_2px_black]">
{children}
</span>
<div className="absolute bottom-1 left-0 -z-10 h-px w-full bg-orange" />
</div>
)
const KeycardFeatures = () => {
return (
<section className="max-w-[1352px] px-3 pt-[120px] lg:mx-auto lg:pt-[200px] min-[1512px]:px-0">
<h1 className="max-w-[665px] font-lora text-32 text-white-95">
Join the <UnderlinedWord>open source</UnderlinedWord> revolution of the
most <UnderlinedWord>modular</UnderlinedWord> and{' '}
<UnderlinedWord>future proof</UnderlinedWord> hardware wallet system
ever conceived.
</h1>
<div className="grid grid-cols-1 gap-5 pt-20 lg:grid-cols-4 lg:gap-6">
{features.map((feature, index) => (
<div
key={index}
className={cx([
'relative flex flex-col justify-between overflow-clip rounded-28 bg-white-3 outline outline-1 outline-white-8',
feature.className,
])}
>
{feature.image && (
<div
className={cx([
'flex items-center justify-center',
feature.imageClassName,
])}
>
<Image
src={feature.image}
alt={feature.title}
className="rounded-12 object-cover"
width={500}
height={500}
/>
</div>
)}
<div className="p-6 pt-5 lg:pt-6">
<div>
<h3 className="mb-[6px] font-lora text-24 font-400">
{feature.title}
</h3>
{feature.description && (
<p className="mb-6 text-16 font-300 text-white-60">
{feature.description}
</p>
)}
{feature.badge && (
<Tag
gradient={feature.badge.gradient}
icon={
feature.badge.icon && createElement(feature.badge.icon)
}
>
{feature.badge.text}
</Tag>
)}
{feature.button && (
<ButtonLink
href="https://github.com/status-im/status-keycard"
className="mt-4 font-500 [&_path]:hover:fill-white-dark"
variant="white"
icon={<feature.button.icon />}
>
{feature.button.text}
</ButtonLink>
)}
</div>
</div>
</div>
))}
</div>
</section>
)
}
export { KeycardFeatures }
@@ -0,0 +1,56 @@
import { Button } from '~components/button'
import { ButtonLink } from '~components/button-link'
import { GetNotifiedDialog } from '~components/get-notified-dialog'
import Image from 'next/image'
const KeycardShell = () => {
return (
<section className="relative flex flex-col justify-end overflow-hidden rounded-b-28 border border-t-0 border-white-8 bg-white-3 backdrop-blur-[20px] full-view-port lg:flex-row lg:items-center lg:justify-normal lg:remove-full-view-port">
<div className="hidden max-h-[510px] flex-1 items-start justify-center overflow-hidden lg:flex">
<Image
src="/assets/keycard-shell.png"
alt="Keycard Shell Hardware Wallet"
width="900"
height="600"
className="object-contain"
priority
/>
</div>
<Image
src="/assets/keycard-shell-mobile.png"
alt="Keycard Shell Hardware Wallet"
width="900"
height="600"
className="self-end lg:hidden"
priority
/>
<div className="relative z-10 -mt-16 flex max-w-[434px] flex-col px-5 pb-8 pt-0 lg:mt-0 lg:py-20 lg:pl-0">
<p className="pb-2 text-24 font-600 text-white-95">
keycard <span className="font-200">shell</span>
</p>
<h1 className="flex pb-8 font-lora text-32 font-400 lg:pb-4 lg:text-48">
One device for all
<br />
your Keycards
</h1>
<p className="pb-8 text-20 font-300 text-white-80">
A modular and stateless hardware wallet that
<br /> relies on Keycard as the secure element.
</p>
<div className="flex gap-4">
<GetNotifiedDialog>
<Button>Get notified</Button>
</GetNotifiedDialog>
<ButtonLink href="/keycard-shell" variant="secondary">
Learn more
</ButtonLink>
</div>
<p className="pt-6 text-16 font-300 text-white-60">Coming 2025</p>
</div>
</section>
)
}
export { KeycardShell }
@@ -0,0 +1,29 @@
'use client'
import { GetNotifiedDialog } from '~components/get-notified-dialog'
import { useState } from 'react'
import * as Dialog from '../../_components/dialog'
export const ThankYouDialog = () => {
const [open, setOpen] = useState(false)
// TODO - Check on load for some parameter in the URL from shopify in order to open the dialog
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Content>
<Dialog.Success
title="Thanks for choosing Keycard!"
description="You have successfully purchased a Keycard set. It will arrive shortly!"
image="/assets/sign-up-success.png"
onClose={() => setOpen(false)}
footer={{
title: 'Want to up your game?',
description: 'Keycard Shell is coming 2025',
Dialog: GetNotifiedDialog,
buttonText: 'Get notified',
}}
/>
</Dialog.Content>
</Dialog.Root>
)
}
+11
View File
@@ -0,0 +1,11 @@
type Props = {
children: React.ReactNode
}
export default function HomepageLayout({ children }: Props) {
return (
<div className="relative m-auto mt-20 w-full max-w-[1512px] lg:mt-[92px]">
{children}
</div>
)
}
+21
View File
@@ -0,0 +1,21 @@
import { BuyCards } from '~/app/(homepage)/_components/buy-cards'
import { ComparisonTable } from '~/app/(homepage)/_components/comparison-table'
import { Faqs } from '~/app/(homepage)/_components/faqs'
import { Hero } from '~/app/(homepage)/_components/hero'
import { KeycardFeatures } from '~/app/(homepage)/_components/keycard-features'
import { KeycardShell } from '~/app/(homepage)/_components/keycard-shell'
import { ThankYouDialog } from './_components/thank-you-dialog'
export default async function HomePage() {
return (
<>
<ThankYouDialog />
<Hero />
<KeycardShell />
<KeycardFeatures />
<ComparisonTable />
<Faqs />
<BuyCards />
</>
)
}
@@ -1,22 +0,0 @@
'use client'
import { BuyKeycardDialog } from '~components/buy-keycard-dialog'
import { useEffect, useState } from 'react'
export const AutoOpenBuyKeycardDialog = () => {
const [open, setOpen] = useState(false)
useEffect(() => {
const buyKeycard = new URLSearchParams(window.location.search).get(
'buyKeycard',
)
if (!buyKeycard || buyKeycard !== 'true') {
return
}
setOpen(true)
}, [])
return <BuyKeycardDialog open={open} onOpenChange={setOpen} />
}
@@ -1,248 +0,0 @@
import {
CheckIcon,
FirmwareIcon,
InfoIcon,
RemoveIcon,
} from '@status-im/icons/20'
import { KEYCARD_PRODUCTS } from '~/app/_constants/shopify/products'
import { formatPrice } from '~/app/_utils/format-price'
import { Tag } from '~components/tag'
import { Tooltip } from '~components/tooltip'
import { createElement } from 'react'
const features: Array<Feature> = [
{
name: 'Competitive pricing',
keycard: {
label: formatPrice({
amount: KEYCARD_PRODUCTS.ONE_CARD_SET.price,
}),
},
tangem: {
label: formatPrice({
amount: 50,
}),
},
ledger: {
label: formatPrice({
amount: 80,
}),
},
},
{
name: 'Compatible with Ethereum and EVM-chains',
keycard: {
featured: true,
},
tangem: {
featured: true,
},
ledger: {
featured: true,
},
},
{
name: 'Hardware-level protection',
// tooltip: 'Security certification level',
keycard: { badge: { text: 'EAL 6+', gradient: true, icon: FirmwareIcon } },
tangem: { badge: { text: 'EAL 6+', gradient: true, icon: FirmwareIcon } },
ledger: { badge: { text: 'EAL 5+' } },
},
{
name: 'Mobile friendly',
keycard: {
featured: true,
},
tangem: {
featured: true,
},
ledger: {
featured: false,
},
},
{
name: 'Works on desktop',
keycard: {
featured: true,
},
tangem: {
featured: false,
},
ledger: {
featured: true,
},
},
{
name: 'Many cards, many keys',
keycard: {
featured: true,
// tooltip: 'No limit on master keys',
},
tangem: {
featured: true,
// tooltip: 'No limit on master keys',
},
ledger: {
featured: false,
},
// tooltip: 'No limit on master keys',
},
{
name: 'Easy back up of master keys',
keycard: {
featured: true,
},
tangem: {
featured: true,
},
ledger: {
featured: false,
},
},
{
name: 'Open ecosystem',
// tooltip: 'Part of an open ecosystem',
keycard: {
featured: true,
},
tangem: {
featured: false,
},
ledger: {
featured: true,
},
},
{
name: 'Immutable, open-source secure element',
keycard: {
featured: true,
},
tangem: {
featured: false,
},
ledger: {
featured: false,
},
},
] as const
type FeatureVariant = {
label?: string
badge?: {
text: string
icon?: React.FC
gradient?: boolean
}
featured?: boolean | 'n/a'
tooltip?: string
}
type Feature = {
name: string
tooltip?: string
keycard: FeatureVariant
tangem: FeatureVariant
ledger: FeatureVariant
}
const FeatureInfo = ({
variant,
}: {
variant: FeatureVariant | undefined | string
}) => {
if (!variant || typeof variant === 'string') return null
if (variant.featured === true) return <CheckIcon className="text-orange" />
if (variant.featured === false)
return <RemoveIcon className="size-4 text-white-60" />
if (variant.featured === 'n/a') return <p className="text-white-60">n/a</p>
if (variant.label)
return <p className="font-400 text-white-95">{variant.label}</p>
if (variant.badge) {
return (
<Tag
gradient={variant.badge.gradient}
icon={variant.badge.icon && createElement(variant.badge.icon)}
size="small"
>
{variant.badge.text}
</Tag>
)
}
}
const TooltipInfo = ({
variant,
}: {
variant: FeatureVariant | undefined | string
}) => {
if (typeof variant === 'string') return null
if (!variant || !variant.tooltip) return null
return (
<Tooltip label={variant.tooltip || ''}>
<div className="absolute left-[calc(50%+10px)] flex">
<InfoIcon className="ml-2 flex-shrink-0 text-white-40 transition-colors hover:text-white-60" />
</div>
</Tooltip>
)
}
const ComparisonTable = () => {
const products: Array<keyof Feature> = ['keycard', 'tangem', 'ledger']
return (
<section className="mx-auto max-w-[1352px] pt-[120px] text-white-95 lg:pt-[200px]">
<h2 className="px-3 font-lora text-32 text-white-95 min-[1512px]:px-0">
Going one step further
</h2>
<div className="overflow-x-auto px-3 scrollbar-none min-[1512px]:px-0">
<div className="w-[684px] pt-14 md:w-full lg:w-full lg:pt-20">
<div className="grid grid-cols-[1.33fr,1fr,1fr,1fr] justify-items-center px-6 text-center font-lora text-24 font-400 text-white-90 lg:grid-cols-[2fr,1fr,1fr,1fr] lg:text-24">
<div className="justify-self-start p-4 pb-5 pl-6">Feature</div>
<div className="p-4 pb-5">Keycard</div>
<div className="p-4 pb-5">Tangem</div>
<div className="p-4 pb-5">Ledger</div>
</div>
<div className="rounded-28 border border-white-8 bg-white-4 px-6">
{features.map((feature, index) => {
return (
<div
key={index}
className="grid grid-cols-[1.33fr,1fr,1fr,1fr] border-b border-dashed border-white-12 font-300 first:pt-2 last:border-b-0 last:pb-2 lg:grid-cols-[2fr,1fr,1fr,1fr]"
>
<div className="w-full p-5 first:pl-0 lg:flex lg:items-center">
<span className="relative inline whitespace-normal">
{feature.name.split(' ').slice(0, -1).join(' ')}{' '}
<span className="inline-flex items-center">
{feature.name.split(' ').slice(-1)[0]}
{feature.tooltip && (
<Tooltip label={feature.tooltip}>
<InfoIcon className="ml-2 flex-shrink-0 text-white-40 transition-colors hover:text-white-60" />
</Tooltip>
)}
</span>
</span>
</div>
{products.map(product => (
<div
key={product}
className="relative flex items-center justify-center last:pr-0"
>
<FeatureInfo variant={feature[product]} />
<TooltipInfo variant={feature[product]} />
</div>
))}
</div>
)
})}
</div>
</div>
</div>
</section>
)
}
export { ComparisonTable }
@@ -1,243 +0,0 @@
'use client'
import * as Accordion from '@radix-ui/react-accordion'
import { useIntersectionObserver } from '~/app/_hooks/use-intersection-observer'
import { DownloadStatusForDesktop } from '~components/download-status-for-desktop'
import { DownloadStatusForMobile } from '~components/download-status-for-mobile'
import { FeaturesDisclaimer } from '~components/features-disclaimer'
import { Image } from '~components/image'
import { cx } from 'cva'
import { AnimatePresence, motion } from 'framer-motion'
import { useEffect, useRef, useState } from 'react'
import { match } from 'ts-pattern'
type Props = {
items: Array<{
title: string
description: React.ReactNode | string
image: string
tag?: string
}>
imageClassName?: string
variant: 'desktop' | 'mobile'
}
const LineDivider = () => (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="944" height="2">
<path
stroke="currentColor"
strokeDasharray="3 4"
strokeLinecap="round"
d="M1 1h944"
/>
</svg>
)
const useAutoSwitch = ({
items,
interval,
step,
isVisible,
variant,
}: {
items: Array<{ title: string }>
interval: number
step: number
isVisible: boolean
variant: 'desktop' | 'mobile'
}) => {
const [value, setValue] = useState(items[0].title)
const [width, setWidth] = useState(0)
const counterRef = useRef(0)
useEffect(() => {
let timer: NodeJS.Timeout | null = null
if (isVisible) {
timer = setInterval(() => {
counterRef.current += step
if (counterRef.current >= interval) {
counterRef.current = 0
setWidth(0)
const index = items.findIndex(item => item.title === value)
setValue(items[(index + 1) % items.length].title)
} else {
setWidth((counterRef.current * 100) / interval)
}
}, step)
}
return () => {
if (timer) clearInterval(timer)
}
}, [items, value, interval, step, isVisible])
useEffect(() => {
if (!isVisible) {
setWidth((counterRef.current * 100) / interval)
}
}, [isVisible, interval])
// Reset the value if the variant changes
useEffect(() => {
setValue(items[0].title)
setWidth(0)
counterRef.current = 0
}, [variant, items])
// Reset the counter if the value changes
useEffect(() => {
counterRef.current = 0
}, [value])
return { value, setValue, width }
}
const FeaturesAccordion = (props: Props) => {
const { items, imageClassName, variant } = props
const wrapperRef = useRef<HTMLDivElement | null>(null)
const entry = useIntersectionObserver(wrapperRef, {
rootMargin: '0%',
})
const isVisible = !!entry?.isIntersecting
const { value, setValue, width } = useAutoSwitch({
items,
interval: 16000,
step: 50,
isVisible,
variant,
})
const selected = items.find(item => item.title === value)!
return (
<div
className="hidden items-center justify-between lg:flex"
ref={wrapperRef}
>
<div
className={cx([
'flex flex-1 items-start justify-center gap-6 xl:gap-0',
])}
>
<div className="flex flex-1 flex-col items-start">
<Accordion.Root
type="single"
value={value}
collapsible
onValueChange={setValue}
className="flex w-full max-w-[549px] flex-col gap-0 pt-24 lg:flex-1 lg:pt-0"
>
{items.map(item => {
const isOpen = value === item.title
return (
<Accordion.Item key={item.title} value={item.title}>
<Accordion.Header>
<Accordion.Trigger disabled={isOpen} className="w-full">
<div className="group relative flex items-center gap-2 py-5">
<p
className={cx(
'flex items-center text-left font-lora text-32 font-500 transition-opacity',
!isOpen && 'group-hover:opacity-[50%]',
)}
>
{item.title}
</p>
{item.tag && (
<span className="mt-2 rounded-[32px] border border-dark-8 bg-white-95 px-2 py-0.5 text-12 font-500 text-dark-100">
{item.tag}
</span>
)}
</div>
</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content
className={cx(
'-translate-y-5 overflow-hidden text-16 font-300 text-white-80 data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown',
)}
>
<div className="pt-1">
<p className="text-16 font-300 text-white-60">
{item.description}
</p>
</div>
</Accordion.Content>
<div className="relative w-full">
<div
className="absolute left-0 top-[0.5px] z-[2] h-px rounded-[2px] bg-orange text-white-100 transition-opacity duration-500"
style={{
opacity: isOpen ? 1 : 0,
width: `${width}%`,
}}
/>
<div className="absolute left-0 top-0 z-[1] w-full overflow-hidden text-white-20">
<LineDivider />
</div>
</div>
</Accordion.Item>
)
})}
</Accordion.Root>
<div className="w-full pt-8 lg:max-w-[549px] lg:pt-20">
<FeaturesDisclaimer />
</div>
</div>
{match(variant)
.with('desktop', () => (
<div className="relative flex flex-1 flex-col items-end self-end">
<AnimatePresence>
<motion.div
initial={{ opacity: 0, x: 40 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 40 }}
>
<Image
src={selected.image}
width={1124}
height={716}
alt={selected.title}
className={imageClassName}
quality={75}
/>
</motion.div>
</AnimatePresence>
<DownloadStatusForDesktop className="mt-20 lg:max-w-[549px]" />
</div>
))
.with('mobile', () => (
<div className="relative flex flex-1 flex-col items-end self-end">
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 40 }}
>
<Image
src={selected.image}
width={1124}
height={716}
alt={selected.title}
className={imageClassName}
quality={75}
/>
</motion.div>
</AnimatePresence>
<DownloadStatusForMobile className="mt-20 lg:max-w-[549px]" />
</div>
))
.exhaustive()}
</div>
</div>
)
}
export { FeaturesAccordion }
export type { Props as FeaturesAccordionProps }
@@ -1,60 +0,0 @@
'use client'
import { DownloadStatusForDesktop } from '~components/download-status-for-desktop'
import { FeaturesDisclaimer } from '~components/features-disclaimer'
import { Image } from '~components/image'
type Props = {
items: Array<{
title: string
description: React.ReactNode | string
imageMobile: string
tag?: string
}>
}
const FeaturesSliderDesktop = (props: Props) => {
const { items } = props
return (
<div className="relative block lg:hidden">
<div className="flex snap-x snap-mandatory overflow-x-auto scrollbar-none">
{items.map((feature, index) => {
return (
<div
key={index}
className="z-20 min-w-[calc(100vw-40px)] snap-start px-2"
>
<div className="mx-auto flex h-[80svh] flex-col overflow-clip rounded-[32px] border border-white-12 bg-white-4 px-6 py-5">
<div>
<p className="pb-[6px] text-left font-lora text-24 font-500 text-white-95">
{feature.title}
</p>
<p className="text-16 font-300 text-white-60">
{feature.description}
</p>
</div>
<Image
src={feature.imageMobile}
alt={feature.title}
width={2358}
height={1871}
priority
className="max-w-[calc(70vh*1.26)] translate-x-[-20%]"
/>
</div>
</div>
)
})}
</div>
<div className="w-full px-2 pt-8 lg:max-w-[549px] lg:pt-20">
<FeaturesDisclaimer />
<DownloadStatusForDesktop className="mt-8" />
</div>
</div>
)
}
export { FeaturesSliderDesktop }
@@ -1,39 +0,0 @@
import { KEYCARD_PRODUCTS } from '~/app/_constants/shopify/products'
import { formatPrice } from '~/app/_utils/format-price'
import { Background } from '~components/3d/background'
import { Button } from '~components/button'
import { BuyKeycardDialog } from '~components/buy-keycard-dialog'
const Hero = () => {
return (
<section className="relative flex h-svh overflow-y-clip full-view-port lg:remove-full-view-port 2xl:h-[839px]">
<Background variant="homepage" />
<div className="pointer-events-none relative z-20 flex flex-1 flex-col justify-end px-5 pb-5 align-bottom lg:pb-[43px] xl:px-20">
<div className="flex flex-col items-start justify-between lg:flex-row">
<div className="pointer-events-auto flex max-w-[664px] flex-col lg:self-end">
<h1 className="flex font-lora text-32 font-400 text-white-95 lg:text-44">
Lightweight design, <br /> heavyweight security
</h1>
<p className="max-w-[435px] pb-8 pt-4 text-20 font-300 text-white-80">
Trade swiftly and safely back up your assets with Keycard hardware
wallets.
</p>
<div className="flex items-center gap-5">
<BuyKeycardDialog>
<Button variant="primary">Buy Keycard</Button>
</BuyKeycardDialog>
<p className="text-16 font-300 text-white-60">
From{' '}
{formatPrice({
amount: KEYCARD_PRODUCTS.ONE_CARD_SET.price,
})}
</p>
</div>
</div>
</div>
</div>
</section>
)
}
export { Hero }
@@ -1,155 +0,0 @@
import { ExternalIcon } from '@status-im/icons/20'
import { Image } from '~components/image'
import { Link } from '~components/link'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '~components/tabs'
import { FeaturesAccordion } from './features-accordion'
import { FeaturesSliderDesktop } from './features-slider-desktop'
import { FeaturesSliderMobile } from './features-slider-mobile'
const featuresMobile = [
{
title: 'Send assets and collectibles',
description: (
<>
Status supports all Ethereum assets on{' '}
<Link
href="https://tokenlists.org/token-list?url=https://ipfs.io/ipns/tokens.uniswap.org"
className="underline decoration-1 underline-offset-2 transition-colors hover:text-orange"
>
Uniswap Labs default token list
</Link>
.
</>
),
image: '/assets/keycard/slider/status-app-mobile-send.png',
imageMobile: '/assets/keycard/slider/iphone-send.png',
},
{
title: 'Swap',
description: 'Swap your favourite assets on Mainnet, Arbitrum or Optimism.',
image: '/assets/keycard/slider/status-app-mobile-swap.png',
imageMobile: '/assets/keycard/slider/iphone-swap.png',
},
{
title: 'Bridge',
description: 'Bridge assets across Ethereums most popular chains. ',
image: '/assets/keycard/slider/status-app-mobile-bridge.png',
imageMobile: '/assets/keycard/slider/iphone-bridge.png',
},
{
title: 'Dapp interactions',
description: 'Connect to your favourite dapps using Wallet Connect.',
tag: 'SOON',
image: '/assets/keycard/slider/status-app-mobile-dapp.png',
imageMobile: '/assets/keycard/slider/iphone-dapp.png',
},
]
const featuresDesktop = [
{
title: 'Send assets and collectibles',
description: (
<>
Status supports all Ethereum assets on{' '}
<Link
href="https://tokenlists.org/token-list?url=https://ipfs.io/ipns/tokens.uniswap.org"
className="underline decoration-1 underline-offset-2 transition-colors hover:text-orange"
>
Uniswap Labs default token list
</Link>
.
</>
),
image: '/assets/keycard/slider/status-app-desktop-send.png',
imageMobile: '/assets/keycard/slider/status-app-desktop-send.png',
},
{
title: 'Swap',
description: 'Swap your favourite assets on Mainnet, Arbitrum or Optimism.',
image: '/assets/keycard/slider/status-app-desktop-swap.png',
imageMobile: '/assets/keycard/slider/status-app-desktop-swap.png',
},
{
title: 'Bridge',
description: 'Bridge assets across Ethereums most popular chains. ',
image: '/assets/keycard/slider/status-app-desktop-bridge.png',
imageMobile: '/assets/keycard/slider/status-app-desktop-bridge.png',
},
{
title: 'Dapp interactions',
description: (
<>
Connect to your favourite dApps with the{' '}
<Link
href="https://github.com/status-im/status-web/tree/main/apps/connector"
className="underline transition-colors hover:text-orange"
>
Status Connector
</Link>{' '}
web extension.
</>
),
tag: 'SOON',
image: '/assets/keycard/slider/status-app-desktop-dapp.png',
imageMobile: '/assets/keycard/slider/status-app-desktop-dapp.png',
},
]
const Integration = () => {
return (
<section className="mt-[200px]">
<div className="flex justify-center border-y border-y-white-8 bg-white-4 remove-full-view-port lg:full-view-port">
<div className="w-full max-w-[1512px] p-20 px-0 pb-5 lg:px-20 lg:pb-20">
<div className="flex max-w-[550px] flex-col gap-2 px-3 pb-8 lg:px-0">
<div className="flex flex-wrap items-center gap-2">
<Image
width={109}
height={32}
alt="Status"
src="/assets/keycard/status-logo.png"
/>
<Link className="group" href="/">
<div className="flex items-center gap-1 rounded-16 border border-dashed border-[#FF640020] bg-[#FF640010] py-[5px] pl-4 pr-3 text-14 font-300 text-orange transition-colors group-hover:bg-[#FF640020]">
Keycard features in beta
<ExternalIcon />
</div>
</Link>
</div>
<h2 className="pt-1 font-lora text-32 font-400 text-white-95">
Seamless integration with Status
</h2>
<p className="text-20 font-300 text-white-60">
Keycard works straight out of the box with the Status app.
<br />
No need to pair it, it just works!
</p>
</div>
<Tabs defaultValue="mobile">
<TabsList className="mx-3 lg:mx-0">
<TabsTrigger value="mobile">On mobile</TabsTrigger>
<TabsTrigger value="desktop">On desktop</TabsTrigger>
</TabsList>
<TabsContent value="desktop" className="pt-20">
<FeaturesAccordion
variant="desktop"
items={featuresDesktop}
imageClassName="absolute top-0 min-w-[808px] translate-x-[45%] xl:translate-x-[43%] 2xl:translate-x-[33%] translate-y-[-87%] xl:min-w-[1008px] right-0 2xl:min-w-[1178px]"
/>
<FeaturesSliderDesktop items={featuresDesktop} />
</TabsContent>
<TabsContent value="mobile" className="pt-20">
<FeaturesAccordion
variant="mobile"
items={featuresMobile}
imageClassName="absolute top-0 translate-x-[30%] xl:translate-x-0 max-w-[604px] translate-y-[-100%] right-0"
/>
<FeaturesSliderMobile items={featuresMobile} />
</TabsContent>
</Tabs>
</div>
</div>
</section>
)
}
export { Integration }
@@ -1,45 +0,0 @@
import { Button } from '~components/button'
import { GetNotifiedDialog } from '~components/get-notified-dialog'
import { Image } from '~components/image'
const KeycardShell = () => {
return (
<section className="relative mx-0 mt-[160px] flex flex-col items-center overflow-hidden rounded-28 border border-white-12 lg:mx-6 lg:mt-[200px] lg:flex-row lg:justify-end xl:mx-20">
<Image
src="/assets/bg-keycard-v1.png"
alt="Keycard Shell Hardware Wallet"
width={1400}
height={800}
className="absolute left-0 top-0 hidden size-full object-cover lg:block"
/>
<Image
src="/assets/bg-keycard-mobile-v1.png"
alt="Keycard Shell Hardware Wallet"
width={414}
height={614}
className="relative left-0 top-0 block aspect-[414/614] w-full lg:hidden"
/>
<div className="absolute right-0 top-0 hidden h-full w-[270px] bg-gradient-to-r from-[transparent] to-dark-100 lg:block" />
<div className="relative z-10 mt-[-53%] flex w-full flex-col p-6 pt-0 lg:mt-0 lg:w-auto lg:py-[256px] lg:pr-[229px]">
<p className="flex min-w-[320px] pb-4 font-lora text-32 font-400">
Get ready for the
<br />
future of modular
<br />
hardware security
</p>
<p className="min-w-[320px] pb-8 text-16 font-300 text-white-60">
Revealing soon. Coming 2025
</p>
<GetNotifiedDialog>
<Button>Get notified</Button>
</GetNotifiedDialog>
</div>
</section>
)
}
export { KeycardShell }
@@ -1,63 +0,0 @@
import { KEYCARD_PRODUCTS } from '~/app/_constants/shopify/products'
import { formatPrice } from '~/app/_utils/format-price'
import { Button } from '~components/button'
import { BuyKeycardDialog } from '~components/buy-keycard-dialog'
import { Image } from '~components/image'
import { cx } from 'cva'
const Prefooter = () => {
return (
<section className="relative flex flex-col overflow-hidden px-6 lg:flex-row lg:px-14 lg:pt-[200px] xl:pl-[309px]">
<div className="relative z-20 flex flex-1 flex-col items-start pb-[120px] pt-[320px] text-left lg:py-[200px]">
<p className="pb-2 text-24 font-600 text-orange">keycard</p>
<h2 className="flex pb-4 font-lora text-32 font-400 text-white-95 md:text-48">
Get the card-shaped
<br />
hardware wallet
</h2>
<p className="max-w-[435px] pb-10 text-20 font-300 text-white-80">
Store and trade your crypto in a simple, secure and slim hardware
wallet.
</p>
<div className="flex flex-row items-center gap-5">
<BuyKeycardDialog>
<Button variant="primary">Buy Keycard</Button>
</BuyKeycardDialog>
<p className="text-16 font-300 text-white-60">
{' '}
From{' '}
{formatPrice({
amount: KEYCARD_PRODUCTS.ONE_CARD_SET.price,
})}
</p>
</div>
</div>
<div className="order-1 flex lg:relative lg:right-[-20px] lg:order-2">
<div className="absolute bottom-0 z-10 block h-1/2 w-full -translate-y-1 bg-gradient-to-t from-dark-100 to-[transparent] lg:hidden" />
<Image
src="/assets/keycard/pre-footer.png"
alt="Keycard"
width={744}
height={712}
draggable={false}
className={cx([
'pointer-events-none absolute left-1/2 top-12 z-0 block h-auto max-w-[549px] -translate-x-1/2 select-none lg:relative lg:left-auto lg:top-auto lg:hidden lg:translate-x-0',
'aspect-[744/712] object-contain',
])}
/>
</div>
<div className="absolute bottom-0 z-10 hidden h-40 w-full -translate-y-1 bg-gradient-to-t from-dark-100 to-[transparent] lg:block" />
<div className="absolute bottom-0 right-0 z-10 hidden h-full w-28 bg-gradient-to-r from-[transparent] to-dark-100 lg:block" />
<Image
src="/assets/keycard/pre-footer-raw.png"
alt="Keycard"
width={1387}
height={912}
draggable={false}
className="pointer-events-none absolute bottom-0 right-0 z-0 hidden max-h-[912px] w-full translate-x-44 select-none object-contain lg:block"
/>
</section>
)
}
export { Prefooter }
-18
View File
@@ -1,18 +0,0 @@
import { Footer } from '~components/footer'
import { NavBar } from '~components/nav-bar'
import { NavBarMobile } from '~components/nav-bar-mobile'
type Props = {
children: React.ReactNode
}
export default function WithNavigationLayout({ children }: Props) {
return (
<>
<NavBar />
<NavBarMobile />
{children}
<Footer />
</>
)
}
@@ -1,9 +0,0 @@
type Props = {
children: React.ReactNode
}
export default function StartLayout({ children }: Props) {
return (
<div className="mx-auto mt-[88px] w-full max-w-[1320px]">{children}</div>
)
}
-77
View File
@@ -1,77 +0,0 @@
import { ExternalIcon } from '@status-im/icons/20'
import { ButtonLink } from '~components/button-link'
import { DownloadStatusForDesktop } from '~components/download-status-for-desktop'
import { DownloadStatusForMobile } from '~components/download-status-for-mobile'
import { FeaturesDisclaimer } from '~components/features-disclaimer'
import Image from 'next/image'
export default function StartPage() {
return (
<div className="px-3 pb-[120px] pt-12 md:px-8 lg:px-20 lg:pt-20">
<div className="mb-6 grid grid-flow-row gap-3">
<h1 className="font-lora text-32 font-400 text-white-95 lg:text-48">
Get started with Keycard
</h1>
<p className="font-inter text-20 font-300 text-white-95">
Follow our step-by-step guides to set up your Keycard
</p>
</div>
<div className="mt-14 grid grid-flow-row gap-3">
<h2 className="mb-6 flex justify-start gap-3 font-lora text-32 font-400 text-white-95">
<span className="text-orange">1</span> Download Status
</h2>
<div className="mb-6 flex flex-col gap-5 rounded-28 border border-white-8 bg-white-4 p-3 pt-5">
<Image
src="/assets/status-logo.png"
alt="Status logo"
width={120}
height={32}
className="ml-3"
/>
<div className="flex flex-col gap-3 lg:flex-row">
<DownloadStatusForDesktop title="Desktop" />
<DownloadStatusForMobile title="Mobile" />
</div>
<FeaturesDisclaimer />
</div>
</div>
<h2 className="my-6 flex justify-start gap-3 font-lora text-32 font-400 text-white-95">
<span className="text-orange">2</span> Follow our guides
</h2>
<div className="flex flex-col gap-3 lg:flex-row">
<div className="flex flex-1 flex-col rounded-28 border border-white-8 bg-white-4 p-6 pt-5">
<h3 className="mb-1 font-lora text-24 font-400 text-white-95">
If you never used Status
</h3>
<p className="mb-6 font-inter text-16 font-300 text-white-80">
Learn how to create and store your keys directly on Keycard
</p>
<ButtonLink
href="https://status.app/help/profile/create-a-status-profile-using-keycard"
variant="secondary"
icon={<ExternalIcon />}
className="mt-auto"
>
Get started
</ButtonLink>
</div>
<div className="flex flex-1 flex-col rounded-28 border border-white-8 bg-white-4 p-6 pt-5">
<h3 className="mb-1 font-lora text-24 font-400 text-white-95">
If you already have a Status profile
</h3>
<p className="mb-6 font-inter text-16 font-300 text-white-80">
Learn how to move your keys to Keycard
</p>
<ButtonLink
href="https://status.app/help/profile/secure-your-status-profile-or-wallet-with-keycard"
variant="secondary"
icon={<ExternalIcon />}
className="mt-auto"
>
Get started
</ButtonLink>
</div>
</div>
</div>
)
}
-260
View File
@@ -1,260 +0,0 @@
'use client'
import { Environment } from '@react-three/drei'
import { Canvas } from '@react-three/fiber'
import {
DepthOfField,
EffectComposer,
Vignette,
} from '@react-three/postprocessing'
import { useIntersectionObserver } from '~/app/_hooks/use-intersection-observer'
import { useWindowFocus } from '~/app/_hooks/use-window-focus'
import { Image } from '~components/image'
import { motion } from 'framer-motion'
import { useRef, useState } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { KeycardModel } from './model'
const cardsPositions: Record<
Props['variant'],
Array<{
position: [number, number, number]
rotation: [number, number, number]
speed: number
scale?: number
}>
> = {
'thank-you': [
{
position: [25, 25, -38],
rotation: [-0.3, 0.4, -0.8],
speed: 5.3,
},
{
position: [60, 45, -68],
rotation: [1, 0, 0],
speed: 5.3,
},
{
position: [0, 15, -15],
rotation: [-0.3, -0.6, 2.4],
speed: 1.4,
},
{
position: [14, 4, -15],
rotation: [0, 0, 0.6],
speed: 2.3,
},
{
position: [42, 4, -30],
rotation: [0, 0.4, 1.2],
speed: 2.3,
scale: 1.4,
},
{
position: [-20.5, 0, -27.5],
rotation: [-1.1, 0.8, -0.6],
speed: 2.75,
},
{
position: [-23, 12, -30],
rotation: [0.5, 0.4, 0.6],
speed: 1.45,
},
{
position: [-50.9, 20, -40],
rotation: [0, 0, 0.6],
speed: 4.25,
},
{
position: [-20.9, 35, -40],
rotation: [-1.3, -0.8, 0.6],
speed: 4.25,
},
{
position: [-107.9, 10, -80],
rotation: [2, 0, 0.6],
speed: 2.25,
},
],
homepage: [
{
position: [25, 25, -38],
rotation: [-0.3, 0.4, -0.8],
speed: 5.3,
},
{
position: [60, 45, -68],
rotation: [1, 0, 0],
speed: 5.3,
},
{
position: [0, 15, -15],
rotation: [-0.3, -0.6, 2.4],
speed: 1.4,
},
{
position: [7, 4, -15],
rotation: [0, 0, 0.6],
speed: 2.3,
},
{
position: [42, 4, -30],
rotation: [0, 0.4, 1.2],
speed: 2.3,
scale: 1.4,
},
{
position: [-7.5, 8, -27.5],
rotation: [-1.1, 0.8, -0.6],
speed: 2.75,
},
{
position: [-23, 12, -30],
rotation: [0.5, 0.4, 0.6],
speed: 1.45,
},
{
position: [-50.9, 20, -40],
rotation: [0, 0, 0.6],
speed: 4.25,
},
{
position: [-20.9, 35, -40],
rotation: [-1.3, -0.8, 0.6],
speed: 4.25,
},
{
position: [-107.9, 10, -80],
rotation: [2, 0, 0.6],
speed: 2.25,
},
],
}
type Props = {
variant: 'thank-you' | 'homepage'
}
const Background = (props: Props) => {
const { variant } = props
return (
<ErrorBoundary fallback={<BackgroundImage variant={variant} />}>
<BackgroundWebGL variant={variant} />
</ErrorBoundary>
)
}
export { Background }
// WEBGL Background
const BackgroundWebGL = (props: Props) => {
const { variant } = props
const [isMounted, setIsMounted] = useState(false)
const wrapperRef = useRef<HTMLDivElement | null>(null)
const entry = useIntersectionObserver(wrapperRef, {
rootMargin: '0%',
})
const isVisible = !!entry?.isIntersecting
const isFocused = useWindowFocus()
return (
<div className="absolute size-full">
<motion.div
animate={{ opacity: isMounted ? 0 : 1 }}
transition={{
duration: 0.3,
ease: 'easeInOut',
}}
className="absolute left-0 top-0 z-0 size-full translate-y-[20px]"
>
<Image
priority
src={
variant === 'homepage'
? '/assets/placeholder.png'
: '/assets/placeholder-thank-you.png'
}
alt="Keycard Placeholder"
width={5478}
height={2166}
className="aspect-[5478/2166] h-[74.5%] w-full object-cover blur-2xl"
/>
</motion.div>
<motion.div
ref={wrapperRef}
animate={{
opacity: isMounted ? 1 : 0,
y: isMounted ? 0 : 20,
}}
transition={{
duration: 0.3,
ease: 'easeInOut',
}}
className="absolute left-0 top-0 z-0 size-full"
>
<Canvas
frameloop={isVisible && isFocused ? 'always' : 'demand'}
onCreated={() => {
setIsMounted(true)
}}
gl={{
precision: 'lowp',
preserveDrawingBuffer: true,
powerPreference: 'low-power',
}}
>
<Environment
preset="night"
environmentIntensity={4}
environmentRotation={[0, 14, -23]}
/>
<ambientLight intensity={3.5} />
<EffectComposer multisampling={0} enabled={isVisible}>
<DepthOfField
target={[0, 5, -30]}
focalLength={0.015}
bokehScale={4}
/>
<Vignette eskil={false} offset={0.55} darkness={1} />
</EffectComposer>
<group scale={2} position={[0, -10, 0]}>
{cardsPositions[variant].map((card, index) => (
<group key={index} position={card.position} scale={card.scale}>
<KeycardModel
initialRotation={card.rotation}
speed={card.speed}
/>
</group>
))}
</group>
</Canvas>
</motion.div>
</div>
)
}
// Background image when webgl is not supported
const BackgroundImage = (props: Props) => {
const { variant } = props
return (
<div className="absolute size-full translate-y-[20px]">
<Image
src={
variant === 'homepage'
? '/assets/placeholder.png'
: '/assets/placeholder-thank-you.png'
}
alt="Keycard"
width={5478}
height={2166}
className="aspect-[5478/2166] h-[74.5%] w-full object-cover"
/>
</div>
)
}
-148
View File
@@ -1,148 +0,0 @@
'use client'
import { useGLTF } from '@react-three/drei'
import { extend, useFrame } from '@react-three/fiber'
import React, { useEffect, useRef, useState } from 'react'
import * as THREE from 'three'
import { GLTF } from 'three-stdlib'
extend({ meshTransmissionMaterial: THREE.Mesh })
type GLTFResult = GLTF & {
nodes: {
Cube002: THREE.Mesh
Cube002_1: THREE.Mesh
Cube002_2: THREE.Mesh
}
materials: {
['Card.image.001']: THREE.MeshStandardMaterial
['Card.light.001']: THREE.MeshStandardMaterial
['Material.001']: THREE.MeshStandardMaterial
}
}
useGLTF.preload('/assets/keycard-model.glb')
const KeycardModel = (
props: React.ComponentProps<'group'> & {
speed: number
initialRotation?: [number, number, number]
},
) => {
const { speed, initialRotation = [0, 0, 0], ...rest } = props
const { nodes, materials } = useGLTF(
'/assets/keycard-model.glb',
) as GLTFResult
const group = useRef<THREE.Group>(null)
const [hovered, setHovered] = useState(false)
const initialRotationRef = useRef(new THREE.Euler(...initialRotation))
const currentRotation = useRef(new THREE.Euler(...initialRotation))
const modelPosition = useRef(new THREE.Vector3())
useEffect(() => {
if (group.current) {
group.current.rotation.set(...initialRotation)
currentRotation.current.set(...initialRotation)
}
}, [initialRotation])
useFrame(state => {
if (!group.current) return
const t = state.clock.getElapsedTime() * 0.5
group.current.position.y = Math.sin(t * (speed * 1.5)) * 0.25
if (hovered) {
group.current.getWorldPosition(modelPosition.current)
const screenX = ((state.pointer.x + 1) / 2) * state.viewport.width
const screenY = ((state.pointer.y + 1) / 2) * state.viewport.height
const maxRotation = THREE.MathUtils.degToRad(30)
const rotX = THREE.MathUtils.clamp(
((screenY - state.viewport.height / 2) / state.viewport.height) *
maxRotation *
1.5,
-maxRotation,
maxRotation,
)
const rotY = THREE.MathUtils.clamp(
((screenX - state.viewport.width / 2) / state.viewport.width) *
maxRotation *
1.5,
-maxRotation,
maxRotation,
)
currentRotation.current.x = initialRotationRef.current.x + rotX
currentRotation.current.y = initialRotationRef.current.y + rotY
currentRotation.current.z = initialRotationRef.current.z
} else {
currentRotation.current.x = THREE.MathUtils.lerp(
currentRotation.current.x,
initialRotationRef.current.x,
0.15,
)
currentRotation.current.y = THREE.MathUtils.lerp(
currentRotation.current.y,
initialRotationRef.current.y,
0.15,
)
currentRotation.current.z = THREE.MathUtils.lerp(
currentRotation.current.z,
initialRotationRef.current.z,
0.15,
)
}
group.current.rotation.x = THREE.MathUtils.lerp(
group.current.rotation.x,
currentRotation.current.x,
0.2,
)
group.current.rotation.y = THREE.MathUtils.lerp(
group.current.rotation.y,
currentRotation.current.y,
0.2,
)
group.current.rotation.z = THREE.MathUtils.lerp(
group.current.rotation.z,
currentRotation.current.z,
0.2,
)
})
return (
<group
ref={group}
scale={100}
{...rest}
onPointerOver={e => {
e.stopPropagation()
setHovered(true)
}}
onPointerOut={e => {
e.stopPropagation()
setHovered(false)
}}
>
<mesh
geometry={nodes.Cube002.geometry}
material={materials['Card.image.001']}
/>
<mesh
geometry={nodes.Cube002_1.geometry}
material={materials['Card.light.001']}
/>
<mesh
geometry={nodes.Cube002_2.geometry}
material={materials['Material.001']}
/>
</group>
)
}
export { KeycardModel }
+5 -3
View File
@@ -10,14 +10,14 @@ type Props = {
} & React.ComponentProps<typeof Link>
const buttonStyles = cva({
base: 'inline-flex cursor-pointer gap-[6px] font-500 text-16 select-none items-center rounded-12 border transition-colors w-fit disabled:opacity-[0.3] disabled:cursor-default',
base: 'inline-flex cursor-pointer gap-[6px] text-16 select-none items-center rounded-12 border transition-colors w-fit disabled:opacity-[0.3] disabled:cursor-default',
variants: {
variant: {
primary: 'bg-orange border-white-12 hover:bg-orange-dark text-white-95',
secondary:
'bg-white-4 border-white-8 hover:bg-white-8 hover:border-white-12 text-white-95',
'bg-white-3 border-white-8 hover:bg-white-8 hover:border-white-12 text-white-95',
white: 'bg-white-100 hover:bg-white-dark text-dark-100',
dark: 'border-[transparent] bg-white-4 hover:border-white-8 hover:bg-white-8 text-white-95',
dark: 'border-[transparent] bg-white-3 hover:border-white-8 hover:bg-white-8 text-white-95',
},
withIcon: {
true: 'pl-[14px] pr-[10px] py-2',
@@ -52,6 +52,8 @@ const ButtonLink = (props: Props) => {
...rest
} = props
console.log(props)
return (
<Link
className={cx([
+3 -4
View File
@@ -10,15 +10,15 @@ type Props = {
} & React.ComponentProps<'button'>
const buttonStyles = cva({
base: 'inline-flex font-500 cursor-pointer gap-1 text-16 select-none items-center rounded-12 border transition-all w-fit disabled:opacity-[0.3] disabled:cursor-default',
base: 'inline-flex cursor-pointer gap-1 text-16 select-none items-center rounded-12 border transition-all w-fit disabled:opacity-[0.3] disabled:cursor-default',
variants: {
variant: {
primary:
'bg-orange border-white-12 hover:enabled:bg-orange-dark text-white-95',
secondary:
'bg-white-4 border-white-8 hover:bg-white-8 hover:border-white-12 text-white-95',
'bg-white-8 border-white-12 hover:enabled:bg-white-12 text-white-95',
white: 'bg-white-100 hover:bg-white-dark text-dark-100',
dark: 'border-[transparent] bg-white-4 hover:enabled:border-white-8 hover:enabled:bg-white-8 text-white-95',
dark: 'border-[transparent] bg-white-3 hover:enabled:border-white-8 hover:enabled:bg-white-8 text-white-95',
},
withIcon: {
true: 'pl-[14px] pr-[10px] py-2',
@@ -62,7 +62,6 @@ const Button = forwardRef<HTMLButtonElement, Props>((props, ref) => {
{...rest}
>
{children}
{icon}
</button>
)
})
+191 -169
View File
@@ -2,33 +2,35 @@
import { zodResolver } from '@hookform/resolvers/zod'
import * as Checkbox from '@radix-ui/react-checkbox'
import {
AddIcon,
CheckIcon,
CloseIcon,
InfoIcon,
LabelsIcon,
LoadingIcon,
RemoveIcon,
WorldIcon,
} from '@status-im/icons/20'
import { Image } from '~components/image'
import { RecommendedIcon } from '~icons/recommended'
import { CartInput } from '~/server/shopify/storefront/validation'
import { cx } from 'cva'
import { AnimatePresence, motion } from 'framer-motion'
import Image from 'next/image'
import { useRouter } from 'next/navigation'
import { useMemo, useState } from 'react'
import { useController, useForm } from 'react-hook-form'
import type { SubmitHandler } from 'react-hook-form'
import { z } from 'zod'
import { KEYCARD_PRODUCTS } from '../_constants/shopify/products'
import {
Check,
Close,
Info,
Labels,
Loading,
Minus,
Plus,
Recommended,
World,
} from '../_icons'
import { useShopifyUTMParamsContext } from '../_providers/shopify-utm-params-provider'
import { formatPrice } from '../_utils/format-price'
import { createCart } from '../actions'
import { Button } from './button'
import * as Dialog from './dialog'
import { Form } from './form/form'
import { Tooltip } from './tooltip'
const formSchema = z
const shopifySchema = z
.object({
bundleId: z.enum(['ONE_CARD_SET', 'TWO_CARDS_SET', 'THREE_CARDS_SET']),
quantity: z.number(),
@@ -36,50 +38,87 @@ const formSchema = z
})
.required()
type FormValues = z.infer<typeof formSchema>
type Shopify = z.infer<typeof shopifySchema>
function createCheckoutUrl(values: FormValues, utmParams: URLSearchParams) {
const url = new URL(
`https://get.keycard.tech/cart/${KEYCARD_PRODUCTS[values.bundleId].variantId}:${values.quantity}`,
)
if (values.includeKeycardReader) {
const readerVariant = `${KEYCARD_PRODUCTS.READER.variantId}:1`
url.pathname += `,${readerVariant}`
}
utmParams.forEach((value, key) => {
url.searchParams.append(key, value)
})
return url.toString()
type Bundle = {
id: Shopify['bundleId']
name: string
price: number
cards: number
image: string
tag?: string
}
type Props = {
children?: React.ReactElement
open?: boolean
onOpenChange?: (open: boolean) => void
children: React.ReactElement
}
const bundles: Bundle[] = [
{
id: 'THREE_CARDS_SET',
name: '3 card set',
price: 64,
cards: 3,
image: '/assets/buy/3-card.png',
tag: 'Best deal',
},
{
id: 'TWO_CARDS_SET',
name: '2 card set',
price: 48,
cards: 2,
image: '/assets/buy/2-card.png',
},
{
id: 'ONE_CARD_SET',
name: '1 card set',
price: 25,
cards: 1,
image: '/assets/buy/1-card.png',
},
] as const
const BuyKeycardDialog = (props: Props) => {
const { children, ...rest } = props
const { children } = props
const [open, setOpen] = useState(false)
return (
<Dialog.Root open={open} onOpenChange={setOpen} {...rest}>
{children && <Dialog.Trigger asChild>{children}</Dialog.Trigger>}
<Dialog.Content
onOpenAutoFocus={event => {
event.preventDefault()
const router = useRouter()
const utmParams = useShopifyUTMParamsContext()
const element = event.target as HTMLElement
element.focus()
}}
className="fixed left-1/2 top-1/2 z-50 max-h-screen w-screen max-w-[1136px] -translate-x-1/2 -translate-y-1/2 overflow-auto focus:outline-none data-[state=open]:animate-contentShow lg:w-[90vw] lg:overflow-hidden"
>
const onSubmit: SubmitHandler<Shopify> = async data => {
const products: CartInput = [
{
productId: KEYCARD_PRODUCTS[data.bundleId].productId,
quantity: data.quantity,
},
]
if (data.includeKeycardReader) {
products.push({
productId: KEYCARD_PRODUCTS.READER.productId,
quantity: 1,
})
}
// TODO: consider add some ui to error handling - toast or something similar
const shopifyCartUrl = await createCart(products)
const url = new URL(shopifyCartUrl)
utmParams.forEach((value, key) => {
url.searchParams.append(key, value)
})
router.push(url.toString())
}
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild>{children}</Dialog.Trigger>
<Dialog.Content className="fixed left-1/2 top-1/2 z-[70] max-h-screen w-screen max-w-[1136px] -translate-x-1/2 -translate-y-1/2 overflow-auto focus:outline-none data-[state=open]:animate-contentShow lg:w-[90vw] lg:overflow-hidden">
<Dialog.Description className="sr-only">Buy Keycard</Dialog.Description>
<ShopifyForm />
<ShopifyForm onSubmit={onSubmit} setOpen={setOpen} />
</Dialog.Content>
</Dialog.Root>
)
@@ -87,9 +126,16 @@ const BuyKeycardDialog = (props: Props) => {
export { BuyKeycardDialog }
const ShopifyForm = () => {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
type ShopifyFormProps = {
onSubmit: SubmitHandler<Shopify>
setOpen: (open: boolean) => void
}
const ShopifyForm = (props: ShopifyFormProps) => {
const { onSubmit, setOpen } = props
const form = useForm<Shopify>({
resolver: zodResolver(shopifySchema),
defaultValues: {
bundleId: 'THREE_CARDS_SET',
includeKeycardReader: true,
@@ -97,14 +143,20 @@ const ShopifyForm = () => {
},
mode: 'onTouched',
})
const { formState, watch, setValue } = form
const { isSubmitting } = formState
const router = useRouter()
const utmParams = useShopifyUTMParamsContext()
const {
formState: { isSubmitting },
watch,
setValue,
} = form
const submitHandler: SubmitHandler<Shopify> = async data => {
return onSubmit(data)
}
const selectedBundle = watch('bundleId')
const quantity = watch('quantity')
const includeReader = watch('includeKeycardReader')
const { field } = useController({
control: form.control,
@@ -112,15 +164,15 @@ const ShopifyForm = () => {
})
const total = useMemo(() => {
const bundlePrice = KEYCARD_PRODUCTS[selectedBundle].price
const readerPrice = 0 // includeReader ? 22 : 0
const bundlePrice = bundles.find(b => b.id === selectedBundle)?.price || 0
const readerPrice = includeReader ? 22 : 0
return bundlePrice * quantity + readerPrice
}, [selectedBundle, quantity])
}, [selectedBundle, quantity, includeReader])
return (
<div className="grid h-svh grid-cols-1 gap-6 overflow-auto bg-white-4 p-5 backdrop-blur-[20px] lg:h-auto lg:grid-cols-2 lg:overflow-clip lg:rounded-28 lg:border lg:border-white-12 lg:p-2">
<div className="hidden h-full rounded-20 bg-[#0C0C0C] lg:block">
<div className="grid h-svh grid-cols-1 gap-6 overflow-auto bg-white-3 p-5 backdrop-blur-[20px] lg:h-auto lg:grid-cols-2 lg:rounded-28 lg:border lg:border-white-12 lg:p-2">
<div className="hidden h-full rounded-28 bg-dark-100 lg:block">
<AnimatePresence>
<motion.div
key={selectedBundle}
@@ -131,8 +183,8 @@ const ShopifyForm = () => {
>
<Image
className="w-auto"
src={KEYCARD_PRODUCTS[selectedBundle].image}
alt={`${KEYCARD_PRODUCTS[selectedBundle].name} keycard`}
src={bundles.find(b => b.id === selectedBundle)!.image}
alt={`${bundles.find(b => b.id === selectedBundle)!.name} keycard`}
width={400}
height={300}
priority
@@ -144,86 +196,76 @@ const ShopifyForm = () => {
<div className="flex flex-col justify-start lg:p-6 lg:pl-0">
<div className="flex items-center justify-between">
<Dialog.Title className="font-lora text-32">Buy Keycard</Dialog.Title>
<Dialog.Close asChild>
<Button
variant="secondary"
className="size-10 px-[9px] text-white-95"
aria-label="Close"
>
<CloseIcon className="size-5" />
</Button>
</Dialog.Close>
<Button
variant="secondary"
className="size-10 px-[9px] text-white-95"
onClick={() => setOpen(false)}
>
<Close className="size-5" />
</Button>
</div>
<Form {...form} onSubmit={() => {}}>
<Form {...form} onSubmit={submitHandler}>
<div className="pt-12 lg:pt-10">
<h3 className="pb-2 text-12 text-white-80 lg:pb-3">
SELECT BUNDLE
</h3>
<div className="grid grid-cols-3 gap-4 lg:gap-6">
{Object.entries(KEYCARD_PRODUCTS)
.filter(([title]) => title !== 'READER')
.map(([title, product]) => {
const selected = selectedBundle === title
{bundles.map(bundle => {
const selected = selectedBundle === bundle.id
return (
<button
key={title}
type="button"
onClick={() => {
setValue('bundleId', title as FormValues['bundleId'])
}}
className={cx(
'relative flex flex-col items-start justify-between rounded-20 bg-white-4 px-4 py-3 text-left transition-colors duration-300 hover:[&>span]:-left-1 hover:[&>span]:-top-1 hover:[&>span]:size-[calc(100%+8px)] hover:[&>span]:rounded-[24px]',
return (
<button
key={bundle.name}
type="button"
onClick={() => {
setValue('bundleId', bundle.id)
}}
className={cx(
'relative flex flex-col items-start justify-between rounded-20 bg-white-3 px-4 py-3 text-left transition-colors duration-300 hover:[&>span]:-left-1 hover:[&>span]:-top-1 hover:[&>span]:size-[calc(100%+8px)] hover:[&>span]:rounded-[24px]',
selected ? 'outline outline-4 outline-[transparent]' : '',
)}
>
<span
className={cx([
'absolute border transition-all',
selected
? 'outline outline-4 outline-[transparent]'
: '',
)}
>
<span
className={cx([
'absolute z-0 border transition-all',
selected
? '-left-1 -top-1 size-[calc(100%+8px)] rounded-[24px] border-orange-dark'
: 'left-0 top-0 size-full rounded-20 border-white-12',
])}
/>
? '-left-1 -top-1 size-[calc(100%+8px)] rounded-[24px] border-orange-dark'
: 'left-0 top-0 size-full rounded-20 border-white-12',
])}
/>
<div className="font-300 text-white-60">
{product.cards} card set
</div>
<div className="flex w-full items-center justify-between font-lora text-24 font-400">
{formatPrice({
amount: Number(product.price),
})}
{!!product.tag && (
<Tooltip label={product.tag}>
<div className="z-50 flex size-5 items-center justify-center rounded-full bg-orange">
<RecommendedIcon />
</div>
</Tooltip>
)}
</div>
</button>
)
})
.reverse()}
<div className="font-300 text-white-60">
{bundle.cards} card set
</div>
<div className="flex w-full items-center justify-between font-lora text-24 font-400">
${bundle.price}
{bundle.tag && (
<Tooltip label="Best deal">
<div className="flex size-5 items-center justify-center rounded-full bg-orange">
<Recommended />
</div>
</Tooltip>
)}
</div>
</button>
)
})}
</div>
</div>
<div className="pt-8">
<h3 className="pb-2 text-12 text-white-80">NUMBER OF SETS</h3>
<div className="flex items-center gap-4 rounded-16 border border-white-12 bg-white-4 p-1">
<div className="flex items-center gap-4 rounded-16 border border-white-12 bg-white-3 p-1">
<Button
type="button"
variant="dark"
onClick={() => setValue('quantity', Math.max(1, quantity - 1))}
className="justify-center px-[9px] text-center text-white-100"
disabled={quantity === 1}
aria-label="Decrease quantity"
>
<RemoveIcon />
<Minus />
</Button>
<span className="flex-1 text-center text-16">{quantity}</span>
<Button
@@ -231,27 +273,25 @@ const ShopifyForm = () => {
variant="dark"
onClick={() => setValue('quantity', quantity + 1)}
className="justify-center px-[9px] text-center text-white-100"
aria-label="Increase quantity"
>
<AddIcon />
<Plus />
</Button>
</div>
</div>
<div className="py-8">
<h3 className="mb-2 text-12 text-white-80">DESKTOP SUPPORT</h3>
<div className="flex items-center justify-between space-x-3 rounded-16 border border-white-12 bg-white-4 p-3 pr-4">
<div className="flex items-center justify-between space-x-3 rounded-16 border border-white-12 bg-white-3 p-3 pr-4">
<div className="relative flex items-center justify-start">
<Checkbox.Root
{...form.register('includeKeycardReader')}
id="includeKeycardReader"
className="flex size-6 appearance-none items-center justify-center rounded-[8px] border border-white-20 bg-white-4 outline-none aria-checked:bg-orange aria-checked:hover:bg-orange-dark [&>svg]:aria-checked:text-white-95"
className="flex size-6 appearance-none items-center justify-center rounded-[8px] bg-white-100 outline-none aria-checked:bg-orange aria-checked:hover:bg-orange-dark [&>svg]:aria-checked:text-white-95"
checked={field.value}
onCheckedChange={field.onChange}
aria-label="Include USB-C Keycard reader"
>
<Checkbox.Indicator className="text-white-95">
<CheckIcon className="size-5 text-white-95" />
<Check className="size-5 text-white-95" />
</Checkbox.Indicator>
</Checkbox.Root>
@@ -259,66 +299,42 @@ const ShopifyForm = () => {
className="ml-3 mr-2 text-16 font-300 text-white-95"
htmlFor="includeKeycardReader"
>
Include Keycard Reader (USB-C)
Include USB-C Keycard reader
</label>
<Tooltip
label={
<>
<p className="mb-2.5 font-400">
For now, some features are only available via the Status
Desktop app:
<p className="font-400">
For now, some features are only available on Desktop:
</p>
<ul className="grid list-disc gap-0.5 pl-4">
<li className="font-300">
Support for multiple wallet accounts
</li>
<li className="font-300">
Support for non Status profile key pairs
</li>
<li className="font-300">Factory reset</li>
<li className="font-300">Change PIN</li>
<li className="font-300">Unlock</li>
<li className="font-300">Backup</li>
<ul className="flex list-disc flex-col gap-0.5 pl-4 pt-2">
<li className="font-300">Something here</li>
<li className="font-300">Amazing feature here</li>
<li className="font-300">Great stuff here</li>
</ul>
</>
}
>
<div className="flex">
<InfoIcon className="flex-shrink-0 text-white-40 transition-colors hover:text-white-60" />
<Info className="flex-shrink-0 text-white-40 transition-colors hover:text-white-60" />
</div>
</Tooltip>
</div>
<div className="flex gap-2 text-16 font-300 text-white-80">
<span className="text-green">Free</span>
<span className="line-through">
{' '}
{formatPrice({
amount: Number(KEYCARD_PRODUCTS.READER.price),
})}
</span>
</div>
<div className="text-16 font-300 text-white-80">+$22</div>
</div>
</div>
<div className="rounded-16 border border-white-12 bg-white-4 p-1">
<div className="rounded-16 border border-white-12 bg-white-3 p-1">
<Button
type="submit"
className="w-full justify-center font-500"
onClick={() => {
const checkoutUrl = createCheckoutUrl(
form.getValues(),
utmParams,
)
window.open(checkoutUrl, '_blank', 'noopener')
router.push(`/thank-you?checkoutUrl=${checkoutUrl}`)
}}
disabled={isSubmitting}
>
{isSubmitting ? (
<LoadingIcon className="my-px animate-spin text-white-100" />
<Loading className="my-px animate-spin text-white-100" />
) : (
<>
Checkout <div className="size-1 rounded-full bg-white-40" />
{formatPrice({
amount: total,
})}
Checkout <div className="size-1 rounded-full bg-white-40" /> $
{total}
</>
)}
</Button>
@@ -339,22 +355,28 @@ const ShopifyForm = () => {
height={32}
/>
<Image
src="/assets/buy/amex.png"
alt="American Express"
src="/assets/buy/apple-pay.png"
alt="Apple Pay"
width={52}
height={32}
/>
<Image
src="/assets/buy/google-pay.png"
alt="Google Pay"
width={52}
height={32}
/>
</div>
<div className="mt-10 flex flex-col items-center gap-[10px] rounded-16 border border-dashed border-white-12 bg-white-4 px-4 py-[14px] text-14 text-white-60 lg:flex-row lg:justify-center lg:gap-2">
<div className="mt-10 flex flex-col items-center gap-[10px] rounded-16 border border-dashed border-white-12 bg-white-3 px-4 py-[14px] text-14 text-white-60 lg:flex-row lg:justify-center lg:gap-2">
<div className="flex items-center">
<LabelsIcon className="mr-1 shrink-0 text-white-95" /> Prices
don&apos;t include VAT
<Labels className="mr-1 shrink-0" /> Prices don&apos;t include
VAT
</div>
<div className="hidden size-1 rounded-full bg-white-40 lg:block" />
<div className="flex items-center">
<WorldIcon className="mr-1 shrink-0 text-white-95" />
Express shipping (3-5 days) available
<World className="mr-1 shrink-0" />
Delivery estimate: 3-5 business days
</div>
</div>
</div>
+3 -3
View File
@@ -1,10 +1,10 @@
'use client'
import { CheckIcon, CopyIcon } from '@status-im/icons/20'
import { cx } from 'cva'
import { useEffect, useState } from 'react'
import { onlyText } from 'react-children-utilities'
import { useCopyToClipboard } from '../(with-navigation)/docs/_hooks/use-copy-to-clipboard'
import { Check, Copy } from '../_icons'
import { useCopyToClipboard } from '../docs/_hooks/use-copy-to-clipboard'
export function CodeBlock(props: React.ComponentProps<'figure'>) {
const [, copy] = useCopyToClipboard()
@@ -33,7 +33,7 @@ export function CodeBlock(props: React.ComponentProps<'figure'>) {
)}
aria-label="Copy code"
>
{success ? <CheckIcon /> : <CopyIcon />}
{success ? <Check /> : <Copy />}
</button>
</div>
{props.children}
+3 -3
View File
@@ -1,8 +1,8 @@
import { BulletIcon } from '@status-im/icons/20'
import { Link } from '~components/link'
import { cx } from 'cva'
import { Children, cloneElement, ComponentProps } from 'react'
import { match } from 'ts-pattern'
import { Bullet } from '../_icons/bullet'
import { renderText } from '../_utils/render-text'
import { AnchorLink } from './anchor-link'
import { CodeBlock } from './code-block'
@@ -150,7 +150,7 @@ export const baseComponents = {
{...rest}
className={cx(
blockquoteParagraphTextSize[size],
'mt-5 border-l border-dashed border-white-4 !pt-0 pl-6',
'mt-5 border-l border-dashed border-white-3 !pt-0 pl-6',
)}
>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
@@ -264,7 +264,7 @@ export const baseComponents = {
) => {
const icon = match(props.parent)
.with('ol', () => <Step value={props.order!} />)
.otherwise(() => <BulletIcon />)
.otherwise(() => <Bullet />)
return (
<li className="flex items-start gap-2">
+13 -10
View File
@@ -1,6 +1,6 @@
import * as BaseDialog from '@radix-ui/react-dialog'
import { Image } from '~components/image'
import { cx } from 'cva'
import Image from 'next/image'
import { forwardRef } from 'react'
import { Button } from './button'
@@ -21,7 +21,10 @@ const Overlay = forwardRef<
React.ComponentPropsWithoutRef<typeof BaseDialog.Overlay>
>(({ className, ...props }, ref) => (
<BaseDialog.Overlay
className={cx('fixed inset-0 z-50 bg-dark-60 backdrop-blur-2xl', className)}
className={cx(
'fixed inset-0 z-[70] bg-dark-60 backdrop-blur-2xl',
className,
)}
{...props}
ref={ref}
/>
@@ -48,7 +51,7 @@ Content.displayName = BaseDialog.Content.displayName
type Props = {
title: string
description: string
// image: string
image: string
onClose: () => void
footer: {
title: string
@@ -59,31 +62,31 @@ type Props = {
}
const Success = (props: Props) => {
const { title, description, footer, onClose } = props
const { title, description, image, footer, onClose } = props
return (
<>
<div
className={cx(
'fixed inset-0 top-1/2 z-50 -mt-6 h-[623px] -translate-y-1/2 overflow-y-auto overflow-x-clip focus:outline-none',
'fixed inset-0 top-1/2 z-[70] -mt-6 h-[623px] -translate-y-1/2 overflow-y-auto overflow-x-clip focus:outline-none',
'md:left-1/2 md:mt-0 md:w-[570px] md:-translate-x-1/2',
)}
>
<div className="relative grid place-items-center px-5">
{/* <Image
<Image
src={image}
alt={title}
width={549}
height={623}
className="absolute left-1/2 top-0 z-0 max-w-fit -translate-x-1/2 md:max-w-full"
/> */}
/>
<div className="z-10 flex flex-col items-center pt-72">
<Title asChild>
<h3 className="mb-3 text-center font-lora text-32 text-white-95">
{title}
</h3>
</Title>
<p className="max-w-[490px] pb-8 text-center text-20 font-300 text-white-80">
<p className="mb-10 max-w-[490px] text-center text-20 font-300 text-white-80">
{description}
</p>
<Button variant="secondary" onClick={onClose}>
@@ -105,7 +108,7 @@ const Footer = (props: FooterProps) => {
return (
<div
className={cx(
'fixed inset-x-5 bottom-4 z-50 flex flex-col items-start justify-between gap-2 bg-white-8 p-1',
'fixed inset-x-5 bottom-4 z-[70] flex flex-col items-start justify-between gap-2 bg-white-8 p-1',
'rounded-20 border border-white-12',
'md:bottom-12 md:left-1/2 md:w-[570px] md:-translate-x-1/2 md:flex-row md:items-center md:gap-4 md:pr-4',
)}
@@ -135,4 +138,4 @@ const Footer = (props: FooterProps) => {
)
}
export { Content, Close, Description, Root, Title, Trigger, Success, Footer }
export { Content, Close, Description, Root, Title, Trigger, Success }
@@ -1,225 +0,0 @@
'use client'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import {
AppleIcon,
ChevronDownIcon,
LinuxIcon,
WindowsIcon,
} from '@status-im/icons/20'
import {
STATUS_APPS_DESKTOP_URL,
STATUS_DESKTOP_DOWNLOAD_URL_LINUX,
STATUS_DESKTOP_DOWNLOAD_URL_MACOS_INTEL,
STATUS_DESKTOP_DOWNLOAD_URL_MACOS_SILICON,
STATUS_DESKTOP_DOWNLOAD_URL_WINDOWS,
} from '~/config/routes'
import { downloadUrl } from '../_utils/download-url'
import { Button } from './button'
import { ButtonLink } from './button-link'
type Props = {
show?: 'single' | 'all'
}
export const DownloadDesktopButton = (props: Props) => {
const { show = 'single' } = props
const prefix = 'Download for'
const macos = 'macOS'
const windows = 'Windows'
const linux = 'Linux'
const desktop = 'desktop'
if (show === 'single') {
return (
<>
<div className="hidden macos:contents">
<MacOsPicker>
<>
{prefix} {macos}
</>
</MacOsPicker>
</div>
<div className="hidden windows:contents">
<WindowsDownloadButton>
<>
{prefix} {windows}
</>
</WindowsDownloadButton>
</div>
<div className="hidden linux:contents">
<LinuxDownloadButton>
<>
{prefix} {linux}
</>
</LinuxDownloadButton>
</div>
<div className="hidden ios:contents android:contents unknown:contents">
<DesktopDownloadButton>
<>
{prefix} {desktop}
</>
</DesktopDownloadButton>
</div>
</>
)
}
return (
<div className="flex flex-wrap gap-2">
<div className="order-2 inline-flex macos:order-1 ios:order-1">
<MacOsPicker>
<span className="hidden macos:contents ios:contents">
{prefix} {macos}
</span>
</MacOsPicker>
</div>
<div className="order-2 inline-flex windows:order-1 android:order-1">
<WindowsDownloadButton>
<span className="hidden windows:contents android:contents unknown:contents">
{prefix} {windows}
</span>
</WindowsDownloadButton>
</div>
<div className="order-2 inline-flex linux:order-1 android:order-2 unknown:order-1">
<LinuxDownloadButton>
<span className="hidden linux:contents unknown:contents">
{prefix} {linux}
</span>
</LinuxDownloadButton>
</div>
</div>
)
}
type DownloadButtonProps = {
children: React.ReactNode
}
const LinuxDownloadButton = (props: DownloadButtonProps) => {
const { children } = props
return (
<>
<div className="hidden macos:contents windows:contents ios:contents android:contents">
<ButtonLink
variant="secondary"
className="!p-[10px]"
href={STATUS_DESKTOP_DOWNLOAD_URL_LINUX}
aria-label="Download for Linux"
>
<LinuxIcon className="text-white-60" />
</ButtonLink>
</div>
<div className="hidden linux:contents unknown:contents">
<ButtonLink
variant="secondary"
href={STATUS_DESKTOP_DOWNLOAD_URL_LINUX}
aria-label="Download for Linux"
icon={<LinuxIcon className="text-white-60" />}
>
{children}
</ButtonLink>
</div>
</>
)
}
const WindowsDownloadButton = (props: DownloadButtonProps) => {
const { children } = props
return (
<>
<div className="hidden macos:contents linux:contents ios:contents unknown:contents">
<ButtonLink
variant="secondary"
className="!p-[10px]"
href={STATUS_DESKTOP_DOWNLOAD_URL_WINDOWS}
aria-label="Download for Windows"
>
<WindowsIcon className="text-white-60" />
</ButtonLink>
</div>
<div className="hidden windows:contents android:contents">
<ButtonLink
variant="secondary"
href={STATUS_DESKTOP_DOWNLOAD_URL_WINDOWS}
aria-label="Download for Windows"
icon={<WindowsIcon className="text-white-60" />}
>
{children}
</ButtonLink>
</div>
</>
)
}
const MacOsPicker = (props: DownloadButtonProps) => {
const { children } = props
return (
<DropdownMenu.Root modal={false}>
<DropdownMenu.Trigger asChild>
<Button
variant="secondary"
icon={
<div className="ml-1 flex size-[14px] items-center justify-center rounded-full bg-white-12">
<ChevronDownIcon />
</div>
}
>
<AppleIcon className="text-white-60" /> {children}
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
sideOffset={4}
align="start"
className="flex w-[225px] flex-col gap-1 rounded-12 border border-white-12 bg-dark-60 p-1"
>
<DropdownMenu.Item
className="cursor-pointer rounded-[8px] bg-white-4 px-3 py-1 transition-colors hover:bg-white-8"
onSelect={() =>
downloadUrl(STATUS_DESKTOP_DOWNLOAD_URL_MACOS_SILICON)
}
>
Apple Silicon
</DropdownMenu.Item>
<DropdownMenu.Item
className="cursor-pointer rounded-[8px] bg-white-4 px-3 py-1 transition-colors hover:bg-white-8"
onSelect={() =>
downloadUrl(STATUS_DESKTOP_DOWNLOAD_URL_MACOS_INTEL)
}
>
Intel
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
)
}
const DesktopDownloadButton = (props: DownloadButtonProps) => {
const { children } = props
return (
<>
<div className="hidden ios:contents android:contents unknown:contents">
<ButtonLink
variant="secondary"
href={STATUS_APPS_DESKTOP_URL}
aria-label="Download for desktop"
>
{children}
</ButtonLink>
</div>
</>
)
}

Some files were not shown because too many files have changed in this diff Show More