Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f9ab08b9c | ||
|
|
0b666ba5bf | ||
|
|
adec63d51c | ||
|
|
be68e34701 | ||
|
|
45b608a45c | ||
|
|
0fd7e2310a | ||
|
|
59298982ef | ||
|
|
22f3362609 | ||
|
|
f58fb1b0ab |
@@ -1 +1 @@
|
||||
pnpx lint-staged
|
||||
pnpm dlx lint-staged
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
20.18.0
|
||||
@@ -0,0 +1,7 @@
|
||||
; 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
|
||||
@@ -25,7 +25,7 @@
|
||||
// note: config https://github.com/microsoft/vscode-js-debug/blob/e620efe1841742402a332b532df2aa2ec95361f6/OPTIONS.md
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"command": "yarn dev -- --port 3030",
|
||||
"command": "NODE_OPTIONS='' pnpm dev --port 3030",
|
||||
"serverReadyAction": {
|
||||
"pattern": "\\s- Local:\\s+(https?://\\S+)",
|
||||
"action": "startDebugging",
|
||||
|
||||
@@ -5,9 +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 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.
|
||||
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/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/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.
|
||||
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.
|
||||
|
||||
## Card requirements
|
||||
|
||||
|
||||
@@ -30,6 +30,5 @@ 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:
|
||||
- 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)
|
||||
- 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)
|
||||
|
||||
@@ -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/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)
|
||||
* [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)
|
||||
|
||||
## Tools
|
||||
|
||||
* [Keycard CLI](https://github.com/status-im/keycard-cli)
|
||||
* [Keycard Desktop](https://github.com/choppu/keycard-desktop)
|
||||
* [Keycard CLI](https://github.com/keycard-tech/keycard-cli)
|
||||
* [Keycard Desktop](https://github.com/choppu/keycard-desktop)
|
||||
|
||||
@@ -33,10 +33,10 @@ When however the data to be signed contains `uint256 amount` and `address curren
|
||||
|
||||

|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
|
||||

|
||||
|
||||
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,5 +1,5 @@
|
||||
---
|
||||
title: Keycard Presale Website - Privacy Policy
|
||||
title: Keycard - 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,5 +1,5 @@
|
||||
---
|
||||
title: Keycard Presale - Terms of Use
|
||||
title: Keycard - 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.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,11 @@ const config = {
|
||||
hostname: 'demo.ghost.io',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'news.keycard.tech',
|
||||
pathname: '/**',
|
||||
},
|
||||
],
|
||||
},
|
||||
transpilePackages: [
|
||||
|
||||
@@ -16,11 +16,19 @@
|
||||
"@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",
|
||||
@@ -29,13 +37,16 @@
|
||||
"github-slugger": "^2.0.0",
|
||||
"google-auth-library": "^9.14.2",
|
||||
"google-spreadsheet": "^4.1.4",
|
||||
"next": "15.0.2",
|
||||
"next": "15.0.3",
|
||||
"next-mdx-remote": "^5.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react": "19.0.0-rc.1",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dom": "19.0.0-rc.1",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"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"
|
||||
},
|
||||
@@ -44,13 +55,13 @@
|
||||
"@types/d3-time-format": "^4.0.3",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.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.2",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
||||
"husky": "^9.1.6",
|
||||
"lint-staged": "^15.2.10",
|
||||
@@ -70,6 +81,9 @@
|
||||
"unified": "^10.1.2",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20.x"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,mjs,ts,tsx}": [
|
||||
"eslint --fix",
|
||||
@@ -81,5 +95,14 @@
|
||||
"*.{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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
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)) {
|
||||
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 550 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 859 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 305 KiB |
|
Before Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 373 KiB |
|
Before Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 347 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 232 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 173 KiB |
@@ -1,115 +0,0 @@
|
||||
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 }
|
||||
@@ -1,422 +0,0 @@
|
||||
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,51 +0,0 @@
|
||||
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 }
|
||||
@@ -1,138 +0,0 @@
|
||||
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 }
|
||||
@@ -1,56 +0,0 @@
|
||||
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 }
|
||||
@@ -1,29 +0,0 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
'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,5 +1,5 @@
|
||||
import { Image } from '~components/image'
|
||||
import { cx } from 'cva'
|
||||
import Image from 'next/image'
|
||||
import { LineGradient } from './linear-gradients'
|
||||
|
||||
type Props = {
|
||||
@@ -0,0 +1,248 @@
|
||||
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,5 +1,5 @@
|
||||
import { ButtonLink } from '~components/button-link'
|
||||
import Image from 'next/image'
|
||||
import { Image } from '~components/image'
|
||||
|
||||
const Customisation = () => {
|
||||
return (
|
||||
@@ -9,10 +9,12 @@ const Customisation = () => {
|
||||
Your brand, same Keycard
|
||||
</h2>
|
||||
<p className="max-w-[549px] pb-8 text-center text-20 font-300 text-white-60">
|
||||
Create your own cards with your design. We support with design,
|
||||
manufacturing, and fulfilment of your cards.
|
||||
Create a custom Keycard with your design. We support with design,
|
||||
manufacturing and fulfilment of your cards.
|
||||
</p>
|
||||
<ButtonLink href="/">Order custom Keycard</ButtonLink>
|
||||
<ButtonLink href="mailto:get@keycard.tech">
|
||||
Order custom Keycard
|
||||
</ButtonLink>
|
||||
<Image
|
||||
src="/assets/keycard/customisation.png"
|
||||
alt="Custom Keycard"
|
||||
@@ -1,4 +1,4 @@
|
||||
import Image from 'next/image'
|
||||
import { Image } from '~components/image'
|
||||
|
||||
const features = [
|
||||
{
|
||||
@@ -9,17 +9,19 @@ const features = [
|
||||
{
|
||||
title: 'Resistant',
|
||||
description:
|
||||
'Resists high (50°) and low (-35°) temperatures, dust and X-Rays.',
|
||||
'Resists high (50°) and low (-35°) temperatures, dust and X-rays.',
|
||||
image: '/assets/keycard/resistant.png',
|
||||
},
|
||||
{
|
||||
title: 'Discreet and light',
|
||||
description: 'Carry it in your wallet, no one will notice you hold crypto.',
|
||||
description:
|
||||
'Carry Keycard in your wallet, no one will notice you hold crypto.',
|
||||
image: '/assets/keycard/discreet-light.png',
|
||||
},
|
||||
{
|
||||
title: 'Water resistant',
|
||||
description: 'Left it in your jeans? It will survive the washing machine!',
|
||||
description:
|
||||
'Left Keycard in your jeans? It will survive the washing machine!',
|
||||
image: '/assets/keycard/water-resistant.png',
|
||||
},
|
||||
]
|
||||
@@ -33,9 +35,9 @@ const Design = () => {
|
||||
{features.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col rounded-28 border border-white-8 bg-white-3 p-6"
|
||||
className="flex flex-col rounded-28 border border-white-8 bg-white-4 p-6"
|
||||
>
|
||||
<div className="text-12 font-400 text-white-80">BY DESIGN</div>
|
||||
<div className="text-12 font-400 text-white-80">0{index + 1}</div>
|
||||
<div className="translate-x-6 translate-y-[-20px] self-end pb-[6px]">
|
||||
<Image
|
||||
src={feature.image}
|
||||
@@ -1,8 +1,10 @@
|
||||
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 { ChevronDown, Discord, X } from '~icons'
|
||||
import { Image } from '~components/image'
|
||||
import { Link } from '~components/link'
|
||||
import { cx } from 'cva'
|
||||
import Image from 'next/image'
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
@@ -12,8 +14,19 @@ const faqs = [
|
||||
},
|
||||
{
|
||||
question: 'Which assets does Keycard support?',
|
||||
answer:
|
||||
'Keycard supports a wide range of cryptocurrencies including Bitcoin, Ethereum, and many ERC-20 tokens.',
|
||||
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>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: 'Are my funds safe if I lose my Keycard?',
|
||||
@@ -26,28 +39,28 @@ const faqs = [
|
||||
'Keycard is compatible with various software wallets. Please check our documentation for a full list.',
|
||||
},
|
||||
{
|
||||
question: 'What happens if I lose access to my Keycard?',
|
||||
answer: 'You can recover your assets using your backup seed phrase.',
|
||||
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.',
|
||||
},
|
||||
]
|
||||
|
||||
const Faqs = () => {
|
||||
return (
|
||||
<section className="mx-auto max-w-[1352px]">
|
||||
<section className="mx-auto max-w-[1352px] pt-[144px]">
|
||||
<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-col lg:pt-[200px]">
|
||||
<div className="block pt-[120px] lg:hidden">
|
||||
<div className="flex w-full flex-1 flex-col lg:w-auto">
|
||||
<div className="flex justify-center pt-[120px] lg:hidden">
|
||||
<Image
|
||||
src="/assets/faqs-mobile.png"
|
||||
src="/assets/faqs-v1.png"
|
||||
alt="FAQ Background"
|
||||
width={736}
|
||||
height={736}
|
||||
className="w-full"
|
||||
width={264}
|
||||
height={488}
|
||||
/>
|
||||
</div>
|
||||
<h1 className="font-lora text-32 text-white-95">
|
||||
<h2 className="mt-14 font-lora text-32 text-white-95 lg:mt-0">
|
||||
Frequently asked questions
|
||||
</h1>
|
||||
</h2>
|
||||
<Accordion.Root
|
||||
className="flex-1 pt-8 lg:max-w-[549px] lg:pt-14"
|
||||
type="single"
|
||||
@@ -70,53 +83,47 @@ const Faqs = () => {
|
||||
>
|
||||
{faq.question}
|
||||
<div className="rounded-[10px] border border-white-12 bg-white-8 p-[6px]">
|
||||
<ChevronDown className="text-white-95 transition-transform duration-300 ease-[cubic-bezier(0.87,_0,_0.13,_1)] group-data-[state=open]:rotate-180" />
|
||||
<ChevronDownIcon 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 text-16 font-300 text-white-80 data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown',
|
||||
'overflow-hidden pr-14 text-16 font-300 text-white-80 data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown',
|
||||
)}
|
||||
>
|
||||
<span className="flex pt-4">{faq.answer}</span>
|
||||
<div className="pt-4">{faq.answer}</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion.Root>
|
||||
</div>
|
||||
<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" />
|
||||
<div className="flex w-full flex-col lg:w-auto lg:items-center">
|
||||
<div className="relative hidden lg:block">
|
||||
<Image
|
||||
src="/assets/faqs.png"
|
||||
src="/assets/faqs-v1.png"
|
||||
alt="FAQ Background"
|
||||
width={736}
|
||||
height={736}
|
||||
height={488}
|
||||
width={264}
|
||||
className="relative"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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="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="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 our community on Discord or
|
||||
X.
|
||||
Reach out to our team or engage with us on X.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<ButtonLink href="/" variant="secondary">
|
||||
Get in touch
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
href="https://discord.com" // TODO: fix this link
|
||||
href="mailto:support@keycard.tech"
|
||||
variant="secondary"
|
||||
className="px-[9px]"
|
||||
aria-label="Keycard on Discord"
|
||||
>
|
||||
<Discord />
|
||||
Get in touch
|
||||
</ButtonLink>
|
||||
<ButtonLink
|
||||
href="https://x.com/keycard_"
|
||||
@@ -124,7 +131,7 @@ const Faqs = () => {
|
||||
className="px-[9px]"
|
||||
aria-label="Keycard on X"
|
||||
>
|
||||
<X />
|
||||
<TwitterIcon />
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,243 @@
|
||||
'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 }
|
||||
@@ -0,0 +1,60 @@
|
||||
'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 }
|
||||
@@ -6,42 +6,39 @@ import {
|
||||
STATUS_MOBILE_F_DROID_URL,
|
||||
STATUS_MOBILE_GOOGLE_PLAY_URL,
|
||||
} from '~/config/routes'
|
||||
import { FeaturesDisclaimer } from '~components/features-disclaimer'
|
||||
import { Image } from '~components/image'
|
||||
import { Link } from '~components/link'
|
||||
import Image from 'next/image'
|
||||
import { match } from 'ts-pattern'
|
||||
|
||||
type Props = {
|
||||
items: Array<{
|
||||
title: string
|
||||
description: string
|
||||
description: React.ReactNode | string
|
||||
imageMobile: string
|
||||
tokens?: string
|
||||
tag?: string
|
||||
}>
|
||||
}
|
||||
|
||||
const FeaturesSlider = (props: Props) => {
|
||||
const FeaturesSliderMobile = (props: Props) => {
|
||||
const { items } = props
|
||||
|
||||
const mobileOS = useMobileOperatingSystem()
|
||||
|
||||
return (
|
||||
<div className="relative block lg:hidden">
|
||||
<div className="relative -mr-2 block lg:hidden">
|
||||
<div className="flex snap-x snap-mandatory overflow-x-auto scrollbar-none">
|
||||
<Image
|
||||
src="/assets/keycard/slider/card.png"
|
||||
alt="left arrow"
|
||||
alt="Card"
|
||||
width={851}
|
||||
height={430}
|
||||
className="absolute right-0 top-1/2 z-10 max-w-[calc(200vw)] -translate-y-1/2 translate-x-1/3"
|
||||
className="absolute left-1/2 top-1/3 max-w-[200vw] -translate-y-1/4 translate-x-[-43%]"
|
||||
/>
|
||||
|
||||
{items.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="z-20 min-w-[calc(100vw-40px)] snap-start px-2"
|
||||
>
|
||||
<div className="mx-auto flex h-full flex-col rounded-[32px] border border-white-12 bg-white-3 px-6 py-5">
|
||||
<div key={index} className="min-w-[calc(100vw-40px)] snap-start px-2">
|
||||
<div className="mx-auto flex h-full flex-col 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}
|
||||
@@ -49,34 +46,29 @@ const FeaturesSlider = (props: Props) => {
|
||||
<p className="text-16 font-300 text-white-60">
|
||||
{feature.description}
|
||||
</p>
|
||||
{feature.tokens && (
|
||||
<div className="flex gap-4 pt-4">
|
||||
<Image
|
||||
alt="Tokens"
|
||||
src={feature.tokens}
|
||||
width={265}
|
||||
height={32}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-28">
|
||||
<div className="z-10 rounded-28 pt-10">
|
||||
<Image
|
||||
src={feature.imageMobile}
|
||||
alt={feature.title}
|
||||
width={489}
|
||||
height={791}
|
||||
width={1125}
|
||||
height={200}
|
||||
priority
|
||||
className="mx-auto w-full max-w-[350px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="hidden w-full px-2 pt-8 lg:block lg:max-w-[549px] lg:pt-20">
|
||||
<FeaturesDisclaimer />
|
||||
</div>
|
||||
{mobileOS && (
|
||||
<div className="mt-14 flex w-full max-w-[549px] flex-col gap-6 rounded-28 border border-white-8 bg-white-3 p-6 pt-5">
|
||||
<div className="mt-14 flex w-full max-w-[549px] flex-col gap-6 rounded-28 border border-white-8 bg-white-4 p-6 pt-5">
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<p className="font-lora text-24 font-400 text-white-95">
|
||||
Download Status for Mobile
|
||||
Download Status for mobile
|
||||
</p>
|
||||
<p className="font-300 text-white-80">
|
||||
Available for iOS or Android
|
||||
@@ -126,4 +118,4 @@ const FeaturesSlider = (props: Props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export { FeaturesSlider }
|
||||
export { FeaturesSliderMobile }
|
||||
@@ -0,0 +1,39 @@
|
||||
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 }
|
||||
@@ -0,0 +1,155 @@
|
||||
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 Ethereum’s 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 Ethereum’s 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 }
|
||||
@@ -0,0 +1,45 @@
|
||||
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 }
|
||||
@@ -3,13 +3,18 @@ const LineGradient = () => {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="484"
|
||||
height="2"
|
||||
viewBox="0 0 484 2"
|
||||
height="1"
|
||||
viewBox="0 0 484 1"
|
||||
>
|
||||
<path stroke="url(#a)" strokeOpacity=".95" strokeWidth="2" d="M484 1H0" />
|
||||
<path
|
||||
stroke="url(#line-use-cases)"
|
||||
strokeOpacity=".95"
|
||||
strokeWidth="2"
|
||||
d="M484 1H0"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="a"
|
||||
id="line-use-cases"
|
||||
x1="-10"
|
||||
x2="484"
|
||||
y1="1.5"
|
||||
@@ -29,8 +34,8 @@ const LinearGradientMobile = () => {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="260"
|
||||
height="2"
|
||||
viewBox="0 0 260 2"
|
||||
height="1"
|
||||
viewBox="0 0 260 1"
|
||||
preserveAspectRatio="none"
|
||||
className="w-full"
|
||||
fill="none"
|
||||
@@ -0,0 +1,63 @@
|
||||
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 }
|
||||
@@ -1,48 +1,50 @@
|
||||
import { FirmwareIcon } from '@status-im/icons/20'
|
||||
import { GithubIcon } from '@status-im/icons/social'
|
||||
import { ButtonLink } from '~components/button-link'
|
||||
import { Image } from '~components/image'
|
||||
import { Tag } from '~components/tag'
|
||||
import { Github, Shield } from '~icons'
|
||||
import Image from 'next/image'
|
||||
|
||||
const SecurityFeatures = () => {
|
||||
return (
|
||||
<section className="relative mt-[100px] overflow-clip px-3 lg:mt-0 lg:px-0">
|
||||
<div className="rounded-28 border border-white-8 bg-white-3 px-6 md:px-0">
|
||||
<section className="relative mt-[100px] overflow-clip px-3 lg:mt-[227px] lg:px-0">
|
||||
<div className="relative overflow-clip rounded-28 border border-white-8 bg-white-4 px-6 md:px-2">
|
||||
<div className="flex flex-col items-center justify-between lg:flex-row">
|
||||
<div className="-mx-6 flex-1 self-start lg:mx-0">
|
||||
<div className="relative -mx-6 flex-1 self-start">
|
||||
<div className="absolute inset-0 z-10 bg-gradient-to-br from-dark-100 to-30%" />
|
||||
<Image
|
||||
src="/assets/keycard/security.png"
|
||||
src="/assets/keycard/chip.png"
|
||||
alt="Keycard security"
|
||||
width={680}
|
||||
height={695}
|
||||
width={580}
|
||||
height={736}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="py-20 pb-6 md:pb-20 md:pl-6 lg:w-1/2 lg:pl-0 lg:pr-[72px]">
|
||||
<div className="py-20 pb-6 md:pb-20 md:pl-6 lg:w-[49%] lg:pl-0 lg:pr-[70px]">
|
||||
<h2 className="pb-[52px] font-lora text-32 lg:pb-14">
|
||||
Unparalleled security
|
||||
Proudly building in the open
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2">
|
||||
<div className="md:border-r md:border-dashed md:border-white-20">
|
||||
<div className="pb-6 md:pb-[28px] md:pl-0 md:pr-6">
|
||||
<h3 className="pb-2 font-lora text-24 font-400 text-white-95">
|
||||
Best in class chip
|
||||
Best-in-class chip
|
||||
</h3>
|
||||
<p className="pb-6 text-16 font-300 text-white-60">
|
||||
Keycard secure element has the highest level of
|
||||
certification provided by Common Criteria.
|
||||
</p>
|
||||
|
||||
<Tag gradient icon={<Shield />}>
|
||||
<Tag gradient icon={<FirmwareIcon />}>
|
||||
EAL6+
|
||||
</Tag>
|
||||
</div>
|
||||
<div className="border-t border-dashed border-white-20 pb-6 pt-5 md:pb-0 md:pl-0 md:pr-6 md:pt-6">
|
||||
<h3 className="pb-2 font-lora text-24 font-400 text-white-95">
|
||||
Non extractable keys
|
||||
Non-extractable keys
|
||||
</h3>
|
||||
<p className="text-16 font-300 text-white-60">
|
||||
The secure element software is non upgradable and no one
|
||||
will ever change its software to change this behaviour.
|
||||
The secure element software is non-upgradable and no one
|
||||
will ever change its software to change its behaviour.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,7 +54,7 @@ const SecurityFeatures = () => {
|
||||
Counterfeit protected
|
||||
</h3>
|
||||
<p className="text-16 font-300 text-white-60">
|
||||
Our open source protocol allows any wallet using the keycard
|
||||
Our open-source protocol allows any wallet using the Keycard
|
||||
to check it's genuine and not counterfeited.
|
||||
</p>
|
||||
</div>
|
||||
@@ -61,16 +63,16 @@ const SecurityFeatures = () => {
|
||||
Fully open source
|
||||
</h3>
|
||||
<p className="pb-6 text-16 font-300 text-white-60">
|
||||
Open source code on an open framework, making it the most
|
||||
Open-source code on an open framework, making it the most
|
||||
open way to design a secure element.
|
||||
</p>
|
||||
<ButtonLink
|
||||
href="https://github.com/status-im/status-keycard"
|
||||
href="https://github.com/keycard-tech/status-keycard"
|
||||
className="font-500 [&_path]:hover:fill-white-dark"
|
||||
variant="white"
|
||||
>
|
||||
<span>View on Github</span>
|
||||
<Github />
|
||||
View on Github
|
||||
<GithubIcon />
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,29 +1,25 @@
|
||||
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 { Recommended } from '~icons'
|
||||
import { Image } from '~components/image'
|
||||
import { RecommendedIcon } from '~icons/recommended'
|
||||
import { cx } from 'cva'
|
||||
import Image from 'next/image'
|
||||
import { Card } from './card'
|
||||
import { LinearGradientMobile } from './linear-gradients'
|
||||
|
||||
const cardSets = [
|
||||
{ count: 3, price: 64, recommended: true },
|
||||
{ count: 2, price: 48 },
|
||||
{ count: 1, price: 25 },
|
||||
]
|
||||
|
||||
const useCases = [
|
||||
{
|
||||
name: 'Vault',
|
||||
description: 'Your most precious tokens in a single card',
|
||||
description: 'Your most precious tokens in a single card.',
|
||||
cardClassName: 'z-20',
|
||||
cardClassNameMobile: 'z-20',
|
||||
captionClassName:
|
||||
'right-1/2 -translate-x-20 xl:-translate-x-1/3 top-[92px]',
|
||||
},
|
||||
{
|
||||
name: 'Backup',
|
||||
description: 'Never worry about losing your Keycards',
|
||||
name: 'Backup vault',
|
||||
description: 'Never worry about losing a Keycard.',
|
||||
cardClassName: 'z-10 -mt-36',
|
||||
cardClassNameMobile: 'z-10',
|
||||
captionClassName: 'left-1/2 translate-x-36 xl:translate-x-1/3 top-10',
|
||||
@@ -31,7 +27,7 @@ const useCases = [
|
||||
},
|
||||
{
|
||||
name: 'Memecoins',
|
||||
description: 'Have peace of mind trading memecoins',
|
||||
description: 'Enjoy peace of mind trading memecoins.',
|
||||
cardClassName: '-mt-40',
|
||||
cardClassNameMobile: '',
|
||||
captionClassName:
|
||||
@@ -48,8 +44,8 @@ const UseCases = () => {
|
||||
Many use cases, multiple Keycards
|
||||
</h2>
|
||||
<p className="max-w-[549px] pb-8 text-20 font-300 text-white-60 lg:mx-auto">
|
||||
By having different Keycards you can store your most valuable card
|
||||
at home while taking your hot wallet with you.
|
||||
Use multiple Keycards to take your hot wallet with you and keep your
|
||||
most valuable assets safe at home.
|
||||
</p>
|
||||
<BuyKeycardDialog>
|
||||
<Button variant="primary">Buy Keycard</Button>
|
||||
@@ -57,24 +53,29 @@ const UseCases = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex justify-start gap-3 py-14 lg:justify-center lg:pb-20">
|
||||
{cardSets.map((set, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex w-40 flex-col justify-between rounded-20 border border-white-12 bg-white-3 px-4 py-3"
|
||||
>
|
||||
<span className="font-300 text-white-60">
|
||||
{set.count} card set
|
||||
</span>
|
||||
<div className="flex w-full items-center justify-between font-lora text-24 font-400">
|
||||
${set.price}
|
||||
{set.recommended && (
|
||||
<span className="flex size-5 items-center justify-center rounded-full bg-orange">
|
||||
<Recommended />
|
||||
</span>
|
||||
)}
|
||||
{Object.entries(KEYCARD_PRODUCTS)
|
||||
.filter(([title]) => title !== 'READER')
|
||||
.map(([title, set], index) => (
|
||||
<div
|
||||
key={title}
|
||||
className="flex w-40 flex-col justify-between rounded-20 border border-white-12 bg-white-4 px-4 py-3"
|
||||
>
|
||||
<span className="text-left text-16 font-300 text-white-60">
|
||||
{index + 1} card set
|
||||
</span>
|
||||
<div className="flex w-full items-center justify-between font-lora text-24 font-400">
|
||||
{formatPrice({
|
||||
amount: Number(set.price),
|
||||
})}
|
||||
{!!set.tag && (
|
||||
<span className="flex size-5 items-center justify-center rounded-full bg-orange">
|
||||
<RecommendedIcon />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
.reverse()}
|
||||
</div>
|
||||
|
||||
{/* Desktop Layout */}
|
||||
@@ -3,5 +3,5 @@ type Props = {
|
||||
}
|
||||
|
||||
export default function KeycardLayout({ children }: Props) {
|
||||
return <div className="m-auto max-w-[1512px]">{children}</div>
|
||||
return <div className="m-auto w-full max-w-[1512px]">{children}</div>
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Metadata } from '~/app/_metadata'
|
||||
import { AutoOpenBuyKeycardDialog } from './_components/auto-open-buy-keycard-dialog'
|
||||
import { ComparisonTable } from './_components/comparison-table'
|
||||
import { Customisation } from './_components/customisation'
|
||||
import { Design } from './_components/design'
|
||||
import { Faqs } from './_components/faqs'
|
||||
import { Hero } from './_components/hero'
|
||||
import { Integration } from './_components/integration'
|
||||
import { KeycardShell } from './_components/keycard-shell'
|
||||
@@ -7,16 +11,25 @@ import { Prefooter } from './_components/pre-footer'
|
||||
import { SecurityFeatures } from './_components/security-features'
|
||||
import { UseCases } from './_components/use-cases'
|
||||
|
||||
export default async function KeycardPage() {
|
||||
export const metadata = Metadata({
|
||||
title: {
|
||||
absolute: 'Keycard: Secure Crypto & Hardware Wallet',
|
||||
},
|
||||
})
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<AutoOpenBuyKeycardDialog />
|
||||
<Hero />
|
||||
<SecurityFeatures />
|
||||
<Design />
|
||||
<Integration />
|
||||
<UseCases />
|
||||
<ComparisonTable />
|
||||
<KeycardShell />
|
||||
<Customisation />
|
||||
<Faqs />
|
||||
<Prefooter />
|
||||
</>
|
||||
)
|
||||
@@ -1,6 +1,9 @@
|
||||
import { baseComponents } from '~/app/_components/content'
|
||||
import { getPostBySlug, getPostSlugs } from '~/app/_lib/ghost'
|
||||
import { Metadata } from '~/app/_metadata'
|
||||
import { Breadcrumbs } from '~/app/docs/_components/breadcrumbs'
|
||||
import { formatDate } from '~/app/_utils/format-date'
|
||||
import { Breadcrumbs } from '~/app/(with-navigation)/docs/_components/breadcrumbs'
|
||||
import { Image } from '~components/image'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { createElement, Fragment } from 'react'
|
||||
import rehypeParse from 'rehype-parse'
|
||||
@@ -8,8 +11,6 @@ import rehypeReact from 'rehype-react'
|
||||
import { unified } from 'unified'
|
||||
import { PostAuthor } from '../_components/post-author'
|
||||
import { PostTag } from '../_components/post-tag'
|
||||
import { getPostBySlug, getPostSlugs } from '../../_lib/ghost'
|
||||
import { formatDate } from '../../_utils/format-date'
|
||||
|
||||
export const revalidate = 3600 // 1 hour
|
||||
export const dynamicParams = true
|
||||
@@ -98,10 +99,12 @@ export default async function BlogDetailPage(props: Props) {
|
||||
</div>
|
||||
|
||||
<div className="mx-auto w-full max-w-[1504px] px-0 py-6 xl:py-10">
|
||||
<img
|
||||
<Image
|
||||
src={post.feature_image!}
|
||||
className="aspect-[374/182] size-full rounded-28 object-cover xl:aspect-[1456/470]"
|
||||
alt={post.feature_image_alt!}
|
||||
className="aspect-[83/38] size-full rounded-28 object-cover"
|
||||
width={664}
|
||||
height={304}
|
||||
alt={post.feature_image_alt! ?? post.title}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Image } from '~components/image'
|
||||
import { cva } from 'cva'
|
||||
import Image from 'next/image'
|
||||
|
||||
type Props = {
|
||||
size: '56' | '32' | '20'
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PostOrPage } from '@tryghost/content-api'
|
||||
import { formatDate } from '~/app/_utils/format-date'
|
||||
import { Image } from '~components/image'
|
||||
import { Link } from '~components/link'
|
||||
import { formatDate } from '../../_utils/format-date'
|
||||
import { PostAuthor } from './post-author'
|
||||
import { PostTag } from './post-tag'
|
||||
|
||||
@@ -19,11 +20,12 @@ export const HighlightedPostCard = (props: Props) => {
|
||||
className="grid grid-cols-1 gap-5 2xl:grid-cols-3 2xl:gap-7"
|
||||
>
|
||||
<div className="col-span-2 w-full flex-[2] shrink-0">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
className="aspect-[366/206] size-full rounded-16 object-cover"
|
||||
<Image
|
||||
className="aspect-[286/161] size-full rounded-16 object-cover"
|
||||
width={1144}
|
||||
height={644}
|
||||
src={post.feature_image!}
|
||||
alt={post.feature_image_alt!}
|
||||
alt={post.feature_image_alt! ?? post.title}
|
||||
/>
|
||||
</div>
|
||||
|
||||