Skip to content

Commit

Permalink
Feat(profile)/transfer (#1124)
Browse files Browse the repository at this point in the history
* πŸ“± Asset page

* πŸ“± Collection scrollbar

* πŸ“± Collection scrollbar

* ✨ Enable send

* ✨ Finalize erc-20 transfer

* Prevent crash calling openSettings and openExecute from old controller

* Apply erc20-send feature flag to cta

* Move posthog set address logic to useAccount hook

* Use useFeatureFlagEnabled hook

* Make transfer work locally

* Remove erc20-transfer feature flag

* Revert erc20-transfer feature flag

* πŸ› Fix scroll bar if too many tokens

* πŸ› Add missing dep

* πŸ› improve code readibility

* πŸ”₯ Remove allowance and improve UX

---------

Co-authored-by: JunichiSugiura <[email protected]>
Co-authored-by: broody <[email protected]>
  • Loading branch information
3 people authored Dec 16, 2024
1 parent 4738bd9 commit cd38a36
Show file tree
Hide file tree
Showing 15 changed files with 332 additions and 227 deletions.
26 changes: 19 additions & 7 deletions packages/controller/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ export default class ControllerProvider extends BaseProvider {
this.profile = profile;
},
methods: {
openSettings: this.openSettings.bind(this),
openPurchaseCredits: this.openPurchaseCredits.bind(this),
openExecute: this.openExecute.bind(this),
openSettings: () => this.openSettings.bind(this),
openPurchaseCredits: () => this.openPurchaseCredits.bind(this),
openExecute: () => this.openExecute.bind(this),
},
rpcUrl: this.rpc.toString(),
username,
Expand Down Expand Up @@ -172,10 +172,15 @@ export default class ControllerProvider extends BaseProvider {
console.error(new NotReadyToConnect().message);
return null;
}
this.iframes.profile?.close();
if (this.iframes.profile?.sendBackward) {
this.iframes.profile?.sendBackward();
} else {
this.iframes.profile?.close();
}
this.iframes.keychain.open();
const res = await this.keychain.openSettings();
this.iframes.keychain.close();
this.iframes.profile?.sendForward?.();
if (res && (res as ConnectError).code === ResponseCodes.NOT_CONNECTED) {
return false;
}
Expand Down Expand Up @@ -224,7 +229,7 @@ export default class ControllerProvider extends BaseProvider {
this.keychain.openPurchaseCredits();
}

private openExecute(calls: any) {
async openExecute(calls: any) {
if (!this.keychain || !this.iframes.keychain) {
console.error(new NotReadyToConnect().message);
return;
Expand All @@ -233,9 +238,16 @@ export default class ControllerProvider extends BaseProvider {
console.error("Profile is not ready");
return;
}
this.iframes.profile.close();
if (this.iframes.profile?.sendBackward) {
this.iframes.profile?.sendBackward();
} else {
this.iframes.profile?.close();
}
this.iframes.keychain.open();
this.keychain.execute(calls);
const res = await this.keychain.execute(calls, undefined, undefined, true);
this.iframes.keychain.close();
this.iframes.profile?.sendForward?.();
return !(res && (res as ConnectError).code === ResponseCodes.NOT_CONNECTED);
}

async delegateAccount() {
Expand Down
10 changes: 10 additions & 0 deletions packages/controller/src/iframe/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,16 @@ export class IFrame<CallSender extends {}> implements Modal {
this.container.style.opacity = "0";
}

sendBackward() {
if (!this.container) return;
this.container.style.zIndex = "9999";
}

sendForward() {
if (!this.container) return;
this.container.style.zIndex = "10000";
}

private resize() {
if (!this.iframe) return;

Expand Down
2 changes: 1 addition & 1 deletion packages/controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export interface Keychain {
username(): string;
fetchControllers(contractAddresses: string[]): Promise<ControllerAccounts>;
openPurchaseCredits(): void;
openExecute(): void;
openExecute(calls: Call[]): Promise<void>;
}
export interface Profile {
navigate(path: string): void;
Expand Down
4 changes: 2 additions & 2 deletions packages/profile/.env.development
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
VITE_CARTRIDGE_API_URL="https://api.cartridge.gg"
VITE_CARTRIDGE_API_URL="http://localhost:8000"
VITE_KEYCHAIN_URL="http://localhost:3001"
VITE_RPC_SEPOLIA="https://api.cartridge.gg/x/starknet/sepolia"
VITE_RPC_SEPOLIA="http://localhost:8001/x/starknet/sepolia"
VITE_POSTHOG_KEY=phc_UWaJajNQ00PjHhveZ81SJ2zVtBicKrzewdZHGiyavQQ
VITE_POSTHOG_HOST=https://profile.cartridge.gg/ingest
4 changes: 2 additions & 2 deletions packages/profile/src/components/context/connection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type ParentMethods = {
close: () => void;
openSettings: () => void;
openPurchaseCredits: () => void;
openExecute: (calls: Call[]) => void;
openExecute: (calls: Call[]) => Promise<void>;
};

const initialState: ConnectionContextType = {
Expand All @@ -40,7 +40,7 @@ const initialState: ConnectionContextType = {
close: () => {},
openSettings: () => {},
openPurchaseCredits: () => {},
openExecute: () => {},
openExecute: async () => {},
},
provider: new RpcProvider({ nodeUrl: import.meta.env.VITE_RPC_SEPOLIA }),
chainId: "",
Expand Down
255 changes: 153 additions & 102 deletions packages/profile/src/components/inventory/collection/asset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
CardTitle,
CopyText,
ExternalIcon,
ScrollArea,
Separator,
} from "@cartridge/ui-next";
import { addAddressPadding, constants } from "starknet";
import {
Expand Down Expand Up @@ -134,111 +136,27 @@ export function Asset() {
icon={asset.imageUrl ?? "/public/placeholder.svg"}
/>

<LayoutContent className="pb-4">
<div className="flex place-content-center">
<div
className="w-[60%] aspect-square rounded-lg bg-cover bg-center flex py-4 place-content-center overflow-hidden p-4"
style={{
backgroundImage: `linear-gradient(0deg, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url(${
asset.imageUrl ?? "/public/placeholder.svg"
})`,
}}
>
<img
className="object-contain"
src={asset.imageUrl ?? "/public/placeholder.svg"}
<LayoutContent>
<ScrollArea>
<div className="flex flex-col h-full flex-1 overflow-y-auto gap-y-4 pb-6">
<Image imageUrl={asset.imageUrl} />
<Description description={asset.description} />
<Properties properties={assets} />
<Details
chainId={chainId as constants.StarknetChainId}
col={col}
asset={asset}
/>
</div>
</div>

{asset.description && (
<Card>
<CardHeader>
<CardTitle>Description</CardTitle>
</CardHeader>

<CardContent>{asset.description}</CardContent>
</Card>
)}

<Card>
<CardHeader className="h-10 flex flex-row items-center justify-between">
<CardTitle>Properties</CardTitle>
</CardHeader>

<CardContent className="bg-background grid grid-cols-3 p-0">
{assets.map((a) => {
const trait = a.trait_type ?? a.trait;
return typeof a.value === "string" ? (
<div
key={`${trait}-${a.value}`}
className="bg-secondary p-3 flex flex-col gap-1"
>
{typeof trait === "string" ? (
<div className="uppercase text-muted-foreground text-2xs font-bold">
{trait}
</div>
) : null}
<div className="text-xs font-medium">
{String(a.value)}
</div>
</div>
) : null;
})}
{Array.from({ length: (assets.length - 1) % 3 }).map(
(_, i) => (
<div
key={`fill-${i}`}
className="bg-secondary p-3 flex flex-col gap-1"
/>
),
)}
</CardContent>
</Card>

<Card>
<CardHeader>
<CardTitle>details</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between">
<div className="text-muted-foreground">Contract</div>
{isPublicChain(chainId) ? (
<Link
to={StarkscanUrl(
chainId as constants.StarknetChainId,
).contract(col.address)}
className="flex items-center gap-1 text-sm"
target="_blank"
>
<div className="font-medium">
{formatAddress(col.address, { size: "sm" })}
</div>
<ExternalIcon size="sm" />
</Link>
) : (
<div>{formatAddress(col.address, { size: "sm" })}</div>
)}
</CardContent>

<CardContent className="flex items-center justify-between gap-4">
<div className="text-muted-foreground whitespace-nowrap">
Token ID
</div>
<div className="font-medium truncate">
{asset.tokenId.startsWith("0x")
? hexToNumber(asset.tokenId as Hex)
: asset.tokenId}
</div>
</CardContent>

<CardContent className="flex items-center justify-between">
<div className="text-muted-foreground">
Token Standard
</div>
<div className="font-medium">{col.type}</div>
</CardContent>
</Card>
</ScrollArea>
</LayoutContent>

<div className="flex flex-col items-center justify-center gap-y-4 pb-6 px-4">
<Separator orientation="horizontal" className="bg-spacer" />
<div className="flex items-center justify-center gap-x-4 w-full">
<Button className="h-10 w-full">Send</Button>
</div>
</div>
</>
);
}
Expand All @@ -247,3 +165,136 @@ export function Asset() {
</LayoutContainer>
);
}

export const Image = ({ imageUrl }: { imageUrl: string | undefined }) => {
return (
<div className="flex place-content-center">
<div
className="w-[60%] aspect-square rounded-lg bg-cover bg-center flex py-4 place-content-center overflow-hidden p-4"
style={{
backgroundImage: `linear-gradient(0deg, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url(${
imageUrl ?? "/public/placeholder.svg"
})`,
}}
>
<img
className="object-contain"
src={imageUrl ?? "/public/placeholder.svg"}
/>
</div>
</div>
);
};

export const Description = ({
description,
}: {
description: string | undefined;
}) => {
if (!description) return null;
return (
<Card>
<CardHeader>
<CardTitle className="uppercase text-[11px] text-quaternary-foreground font-bold tracking-wider">
Description
</CardTitle>
</CardHeader>

<CardContent>{description}</CardContent>
</Card>
);
};

export const Properties = ({
properties,
}: {
properties: Record<string, unknown>[];
}) => {
return (
<Card>
<CardHeader className="h-10 flex flex-row items-center justify-between">
<CardTitle className="uppercase text-[11px] text-quaternary-foreground font-bold tracking-wider">
Properties
</CardTitle>
</CardHeader>

<CardContent className="bg-background grid grid-cols-3 p-0 gap-x-px">
{properties.map((property) => {
const trait = property.trait_type ?? property.trait;
return typeof property.value === "string" ? (
<div
key={`${trait}-${property.value}`}
className="bg-secondary p-3 flex flex-col gap-1"
>
{typeof trait === "string" ? (
<div className="uppercase text-muted-foreground text-2xs font-bold">
{trait}
</div>
) : null}
<div className="text-xs font-medium">
{String(property.value)}
</div>
</div>
) : null;
})}
{Array.from({ length: (properties.length - 1) % 3 }).map((_, i) => (
<div
key={`fill-${i}`}
className="bg-secondary p-3 flex flex-col gap-1"
/>
))}
</CardContent>
</Card>
);
};

export const Details = ({
chainId,
col,
asset,
}: {
chainId: constants.StarknetChainId;
col: Collection;
asset: Asset;
}) => {
return (
<Card>
<CardHeader>
<CardTitle className="uppercase text-[11px] text-quaternary-foreground font-bold tracking-wider">
details
</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between">
<div className="text-muted-foreground">Contract Address</div>
{isPublicChain(chainId) ? (
<Link
to={StarkscanUrl(chainId).contract(col.address)}
className="flex items-center gap-x-1.5 text-sm"
target="_blank"
>
<div className="font-medium">
{formatAddress(col.address, { size: "xs" })}
</div>
<ExternalIcon size="sm" />
</Link>
) : (
<div>{formatAddress(col.address, { size: "sm" })}</div>
)}
</CardContent>

<CardContent className="flex items-center justify-between gap-4">
<div className="text-muted-foreground whitespace-nowrap">Token ID</div>
<div className="font-medium truncate">
{asset.tokenId.startsWith("0x")
? hexToNumber(asset.tokenId as Hex)
: asset.tokenId}
</div>
</CardContent>

<CardContent className="flex items-center justify-between">
<div className="text-muted-foreground">Token Standard</div>
<div className="font-medium">{col.type}</div>
</CardContent>
</Card>
);
};
Loading

0 comments on commit cd38a36

Please sign in to comment.