Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve account connection #314

Merged
merged 4 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tiny-apes-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@starknet-react/core": patch
---

Fix wallet connection when wallet not authorized
5 changes: 5 additions & 0 deletions .changeset/tough-timers-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@starknet-react/core": patch
---

Add hook to detect injected connectors
124 changes: 124 additions & 0 deletions packages/core/src/connectors/discovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import type { StarknetWindowObject } from "get-starknet-core";
import { useCallback, useEffect, useMemo, useState } from "react";

import { Connector } from "./base";
import { injected } from "./helpers";

export type UseInjectedConnectorsProps = {
/** List of recommended connectors to display. */
recommended?: Connector[];
/** Whether to include recommended connectors in the list. */
includeRecommended?: "always" | "onlyIfNoConnectors";
/** How to order connectors. */
order?: "random" | "alphabetical";
};

export type UseInjectedConnectorsResult = {
/** Connectors list. */
connectors: Connector[];
};

export function useInjectedConnectors({
recommended,
includeRecommended = "always",
order = "alphabetical",
}: UseInjectedConnectorsProps): UseInjectedConnectorsResult {
const [injectedConnectors, setInjectedConnectors] = useState<Connector[]>([]);

const refreshConnectors = useCallback(() => {
const wallets = scanObjectForWallets(window);
const connectors = wallets.map((wallet) => injected({ id: wallet.id }));
setInjectedConnectors(connectors);
}, [setInjectedConnectors]);

useEffect(() => {
refreshConnectors();
}, [refreshConnectors]);

const connectors = useMemo(() => {
return mergeConnectors(injectedConnectors, recommended ?? [], {
includeRecommended,
order,
});
}, [injectedConnectors, recommended, includeRecommended, order]);

return { connectors };
}

function mergeConnectors(
injected: Connector[],
recommended: Connector[],
{
includeRecommended,
order,
}: Required<Pick<UseInjectedConnectorsProps, "includeRecommended" | "order">>,
): Connector[] {
const injectedIds = new Set(injected.map((connector) => connector.id));
const allConnectors = injected;
const shouldAddRecommended =
includeRecommended === "always" ||
(includeRecommended === "onlyIfNoConnectors" && injected.length === 0);
if (shouldAddRecommended) {
allConnectors.push(
...recommended.filter((connector) => !injectedIds.has(connector.id)),
);
}

if (order === "random") {
return shuffle(allConnectors);
}
return allConnectors.sort((a, b) => a.id.localeCompare(b.id));
}

function shuffle<T>(arr: T[]): T[] {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
// @ts-ignore: not important
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}

export function scanObjectForWallets(
// biome-ignore lint: window could contain anything
obj: Record<string, any>,
): StarknetWindowObject[] {
return Object.values(
Object.getOwnPropertyNames(obj).reduce<
Record<string, StarknetWindowObject>
>((wallets, key) => {
if (key.startsWith("starknet")) {
const wallet = obj[key];

if (isWalletObject(wallet) && !wallets[wallet.id]) {
wallets[wallet.id] = wallet;
}
}
return wallets;
}, {}),
);
}

// biome-ignore lint: window could contain anything
function isWalletObject(wallet: any): wallet is StarknetWindowObject {
try {
return (
wallet &&
[
// wallet's must have methods/members, see IStarknetWindowObject
"request",
"isConnected",
"provider",
"enable",
"isPreauthorized",
"on",
"off",
"version",
"id",
"name",
"icon",
].every((key) => key in wallet)
);
} catch (err) {}
return false;
}
27 changes: 27 additions & 0 deletions packages/core/src/connectors/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { InjectedConnector } from "./injected";

export function argent(): InjectedConnector {
return new InjectedConnector({
options: {
id: "argentX",
name: "Argent",
},
});
}

export function braavos(): InjectedConnector {
return new InjectedConnector({
options: {
id: "braavos",
name: "Braavos",
},
});
}

export function injected({ id }: { id: string }): InjectedConnector {
return new InjectedConnector({
options: {
id,
},
});
}
29 changes: 6 additions & 23 deletions packages/core/src/connectors/index.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,13 @@
export { Connector } from "./base";
export { InjectedConnector, type InjectedConnectorOptions } from "./injected";

export {
type UseInjectedConnectorsProps,
type UseInjectedConnectorsResult,
useInjectedConnectors,
} from "./discovery";
export {
MockConnector,
type MockConnectorAccounts,
type MockConnectorOptions,
} from "./mock";

import { InjectedConnector } from "./injected";

export function argent(): InjectedConnector {
return new InjectedConnector({
options: {
id: "argentX",
name: "Argent",
icon: {},
},
});
}

export function braavos(): InjectedConnector {
return new InjectedConnector({
options: {
id: "braavos",
name: "Braavos",
icon: {},
},
});
}
export { argent, braavos, injected } from "./helpers";
36 changes: 32 additions & 4 deletions packages/core/src/connectors/injected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,25 @@ export interface InjectedConnectorOptions {
/** The wallet id. */
id: string;
/** Wallet human readable name. */
name: string;
name?: string;
/** Wallet icons. */
icon: ConnectorIcons;
icon?: ConnectorIcons;
}

// Icon used when the injected wallet is not found and no icon is provided.
// question-mark-circle from heroicons
function walletNotFoundIcon(color: string): string {
const encoded = Buffer.from(`
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="${color}">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
</svg>
`).toString("base64");
return `data:image/svg+xml;base64,${encoded}`;
}

const WALLET_NOT_FOUND_ICON_LIGHT = walletNotFoundIcon("black");
const WALLET_NOT_FOUND_ICON_DARK = walletNotFoundIcon("white");

export class InjectedConnector extends Connector {
private _wallet?: StarknetWindowObject;
private _options: InjectedConnectorOptions;
Expand All @@ -33,11 +47,23 @@ export class InjectedConnector extends Connector {
}

get name(): string {
return this._options.name;
return this._options.name ?? this._wallet?.name ?? this._options.id;
}

get icon(): ConnectorIcons {
return this._options.icon;
let defaultIcon = {
dark: WALLET_NOT_FOUND_ICON_DARK,
light: WALLET_NOT_FOUND_ICON_LIGHT,
};

if (this._wallet?.icon) {
defaultIcon = {
dark: this._wallet.icon,
light: this._wallet.icon,
};
}

return this._options.icon ?? defaultIcon;
}

available(): boolean {
Expand Down Expand Up @@ -97,6 +123,8 @@ export class InjectedConnector extends Connector {
const account = this._wallet.account.address;
const chainId = await this.chainId();

this.emit("connect", { account, chainId });

return {
account,
chainId,
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/hooks/useAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,13 @@ export function useAccount({

for (const connector of connectors) {
if (!connector.available()) continue;
const connAccount = await connector.account();

// If the connector is not authorized, `.account()` will throw.
let connAccount;
try {
connAccount = await connector.account();
} catch {}

if (connAccount && connAccount?.address === connectedAccount.address) {
if (state.isDisconnected && onConnect !== undefined) {
onConnect({ address: connectedAccount.address, connector });
Expand Down
1 change: 0 additions & 1 deletion website/app/docs/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,3 @@ export default function DocLayout({ children }: { children: React.ReactNode }) {
);
}


58 changes: 58 additions & 0 deletions website/app/hooks/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from "react";

import { notFound, redirect } from "next/navigation";

import { allHooks } from "@/.contentlayer/generated";
import { Mdx } from "@/components/mdx";
import { DocContainer } from "@/components/container";

type DocPageProps = {
params: {
slug: string[];
};
};

async function getDocFromParams({ params }: DocPageProps) {
const slug = params.slug?.join("/") || "";

if (slug === "") {
const redirectTo = allHooks[0]?.slugAsParams;
return { doc: null, redirectTo };
}

const doc = allHooks.find((doc) => doc.slugAsParams === slug);

if (!doc) {
return { doc: null, redirectTo: null };
}

return { doc, redirectTo: null };
}

export async function generateStaticParams(): Promise<
DocPageProps["params"][]
> {
return allHooks.map((doc) => ({
slug: doc.slugAsParams.split("/"),
}));
}

export default async function DocPage({ params }: DocPageProps) {
const { doc, redirectTo } = await getDocFromParams({ params });

if (!doc) {
if (redirectTo) {
redirect(`/hooks/${redirectTo}`);
} else {
notFound();
}
}

return (
<DocContainer title={doc.title} section="Hooks">
<Mdx code={doc.body.code} />
</DocContainer>
);
}


19 changes: 19 additions & 0 deletions website/app/hooks/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";

import { Sidebar } from "@/components/sidebar";
import { ScrollArea } from "@/components/ui/scroll-area";
import { docsSidebar } from "@/lib/sidebar";

export default function HookLayout({ children }: { children: React.ReactNode }) {
return (
<div className="container flex-1 items-start md:grid md:grid-cols-[220px_minmax(0,1fr)] md:gap-6 lg:grid-cols-[240px_minmax(0,1fr)] lg:gap-10">
<aside className="fixed top-14 z-30 -ml-2 hidden h-[calc(100vh-3.5rem)] w-full shrink-0 md:sticky md:block">
<ScrollArea className="h-full py-6 pl-8 pr-6 lg:py-8">
<Sidebar items={docsSidebar} />
</ScrollArea>
</aside>
{children}
</div >
);
}

1 change: 1 addition & 0 deletions website/components/starknet/connect-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default function ConnectModal() {
onClick={() => connect({ connector })}
disabled={!connector.available()}
>
<img src={connector.icon.dark} className="w-4 h-4 mr-2" />
Connect {connector.name}
</Button>
))}
Expand Down
Loading