Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1d28988253 Version Packages (#8687)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-24 22:39:15 +13:00
5352152835 Connect modal approval changeset (#8686)
Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Yash <[email protected]>
2026-02-24 22:36:53 +13:00
decbbb8f7a Connect modal require approval (#8683)
Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Yash <[email protected]>
2026-02-24 22:01:29 +13:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ecd5ae1feb Version Packages (#8672)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-13 23:39:01 +07:00
FirekeeperandGitHub 1caf6dd460 changeset / fmt (#8671) 2026-02-13 23:34:21 +07:00
19204e4654 [SDK] Fix Safe wallet WalletConnect connection on Berachain (#8670)
Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-13 23:24:37 +07:00
Manan TankandGitHub 674c8d5e6c portal: Fix sidebar spacing on top (#8666) 2026-02-11 17:09:29 +13:00
1eb0cd9314 Feat/partner logo branding (#8668)
Co-authored-by: Claude Opus 4.6 <[email protected]>
2026-02-10 14:52:06 -08:00
19 changed files with 261 additions and 35 deletions
@@ -105,6 +105,7 @@ type PartnerPermission = "PROMPT_USER_V1" | "FULL_CONTROL_V1";
export type Partner = {
id: string;
name: string;
imageUrl?: string;
allowlistedDomains: string[];
allowlistedBundleIds: string[];
permissions: [PartnerPermission];
@@ -3,6 +3,7 @@ import { useParams } from "next/navigation";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
import type { Ecosystem, Partner } from "@/api/team/ecosystems";
import { useDashboardStorageUpload } from "@/hooks/useDashboardStorageUpload";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useAddPartner } from "../../hooks/use-add-partner";
import { PartnerForm, type PartnerFormValues } from "./partner-form.client";
@@ -23,6 +24,8 @@ export function AddPartnerForm({
const teamSlug = params.team_slug as string;
const ecosystemSlug = params.slug as string;
const storageUpload = useDashboardStorageUpload({ client });
const { mutateAsync: addPartner, isPending } = useAddPartner(
{
authToken,
@@ -48,10 +51,23 @@ export function AddPartnerForm({
},
);
const handleSubmit = (
const isUploading = storageUpload.isPending;
const handleSubmit = async (
values: PartnerFormValues,
finalAccessControl: Partner["accessControl"] | null,
) => {
let imageUrl: string | undefined;
if (values.logo) {
try {
const [uri] = await storageUpload.mutateAsync([values.logo]);
imageUrl = uri;
} catch {
toast.error("Failed to upload logo");
return;
}
}
addPartner({
accessControl: finalAccessControl,
allowlistedBundleIds: values.bundleIds
@@ -61,6 +77,7 @@ export function AddPartnerForm({
.split(/,| /)
.filter((d) => d.length > 0),
ecosystem,
imageUrl,
name: values.name,
});
};
@@ -68,7 +85,7 @@ export function AddPartnerForm({
return (
<PartnerForm
client={client}
isSubmitting={isPending}
isSubmitting={isPending || isUploading}
onSubmit={handleSubmit}
submitLabel="Add"
/>
@@ -1,12 +1,13 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon, Trash2Icon } from "lucide-react";
import { useId } from "react";
import { PencilIcon, PlusIcon, Trash2Icon, XIcon } from "lucide-react";
import { useId, useRef } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import type { ThirdwebClient } from "thirdweb";
import type { z } from "zod";
import type { Partner } from "@/api/team/ecosystems";
import { Img } from "@/components/blocks/Img";
import { Button } from "@/components/ui/button";
import {
Form,
@@ -17,11 +18,13 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { ImageUpload } from "@/components/ui/image-upload";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Spinner } from "@/components/ui/Spinner";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { partnerFormSchema } from "../../constants";
import { AllowedOperationsSection } from "./allowed-operations-section";
@@ -118,6 +121,7 @@ export function PartnerForm({
const accessControlId = useId();
const serverVerifierId = useId();
const fileInputRef = useRef<HTMLInputElement>(null);
return (
<Form {...form}>
@@ -150,6 +154,101 @@ export function PartnerForm({
</FormItem>
)}
/>
<FormField
control={form.control}
name="logo"
render={() => {
const removeLogo = form.watch("removeLogo");
const logoFile = form.watch("logo");
const newFilePreview = logoFile
? URL.createObjectURL(logoFile)
: undefined;
const existingImageUrl =
partner?.imageUrl && !removeLogo
? resolveSchemeWithErrorHandler({
client,
uri: partner.imageUrl,
})
: undefined;
const displayUrl = newFilePreview || existingImageUrl;
return (
<FormItem>
<FormLabel>Partner Logo</FormLabel>
<FormControl>
<div>
<input
ref={fileInputRef}
accept="image/png,image/jpeg,image/webp"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
form.setValue("logo", file, {
shouldValidate: true,
});
form.setValue("removeLogo", false);
}
e.target.value = "";
}}
type="file"
/>
{displayUrl ? (
<div className="relative inline-block">
<Img
alt={partner?.name ?? "Partner logo"}
className="size-20 rounded-md border object-contain object-center"
src={displayUrl}
/>
<Button
aria-label="Remove logo"
className="absolute -top-2 -right-2 size-6 rounded-full bg-background p-0 hover:bg-accent"
onClick={() => {
form.setValue("logo", undefined);
form.setValue("removeLogo", true);
}}
size="icon"
type="button"
variant="ghost"
>
<XIcon className="size-3" />
</Button>
<Button
aria-label="Change logo"
className="absolute -bottom-2 -right-2 size-6 rounded-full bg-background p-0 hover:bg-accent"
onClick={() => fileInputRef.current?.click()}
size="icon"
type="button"
variant="ghost"
>
<PencilIcon className="size-3" />
</Button>
</div>
) : (
<ImageUpload
accept="image/png, image/jpeg, image/webp"
className="bg-background"
onUpload={(files) => {
if (files[0]) {
form.setValue("logo", files[0], {
shouldValidate: true,
});
form.setValue("removeLogo", false);
}
}}
/>
)}
</div>
</FormControl>
<FormDescription>
Optional logo for this partner. Used in OTP emails sent to
users authenticating through this partner.
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
defaultValue=""
@@ -3,6 +3,7 @@ import { useParams } from "next/navigation";
import { toast } from "sonner";
import type { ThirdwebClient } from "thirdweb";
import type { Ecosystem, Partner } from "@/api/team/ecosystems";
import { useDashboardStorageUpload } from "@/hooks/useDashboardStorageUpload";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useUpdatePartner } from "../../hooks/use-update-partner";
import { PartnerForm, type PartnerFormValues } from "./partner-form.client";
@@ -25,6 +26,8 @@ export function UpdatePartnerForm({
const teamSlug = params.team_slug as string;
const ecosystemSlug = params.slug as string;
const storageUpload = useDashboardStorageUpload({ client });
const { mutateAsync: updatePartner, isPending } = useUpdatePartner(
{
authToken,
@@ -50,10 +53,31 @@ export function UpdatePartnerForm({
},
);
const handleSubmit = (
const isUploading = storageUpload.isPending;
const handleSubmit = async (
values: PartnerFormValues,
finalAccessControl: Partner["accessControl"] | null,
) => {
// Determine imageUrl based on three states:
// 1. New file uploaded → upload and use new URI
// 2. Explicit removal → send null to clear
// 3. No change → preserve existing partner imageUrl
let imageUrl: string | null | undefined;
if (values.logo) {
try {
const [uri] = await storageUpload.mutateAsync([values.logo]);
imageUrl = uri;
} catch {
toast.error("Failed to upload logo");
return;
}
} else if (values.removeLogo) {
imageUrl = null;
} else {
imageUrl = partner.imageUrl;
}
updatePartner({
accessControl: finalAccessControl,
allowlistedBundleIds: values.bundleIds
@@ -63,6 +87,7 @@ export function UpdatePartnerForm({
.split(/,| /)
.filter((d) => d.length > 0),
ecosystem,
imageUrl,
name: values.name,
partnerId: partner.id,
});
@@ -71,7 +96,7 @@ export function UpdatePartnerForm({
return (
<PartnerForm
client={client}
isSubmitting={isPending}
isSubmitting={isPending || isUploading}
onSubmit={handleSubmit}
partner={partner}
submitLabel="Update"
@@ -154,6 +154,21 @@ const allowedOperationsSchema = z.discriminatedUnion("signMethod", [
export const partnerFormSchema = z
.object({
logo: z
.instanceof(File, {
message: "Please select an image file",
})
.refine(
(file) => ["image/png", "image/jpeg", "image/webp"].includes(file.type),
{
message: "Only PNG, JPG or WEBP images are allowed",
},
)
.refine((file) => file.size <= 500 * 1024, {
message: "Logo size must be less than 500KB",
})
.optional(),
removeLogo: z.boolean().default(false),
accessControl: z
.object({
allowedOperations: z
@@ -8,6 +8,7 @@ import type { Ecosystem, Partner } from "@/api/team/ecosystems";
type AddPartnerParams = {
ecosystem: Ecosystem;
name: string;
imageUrl?: string;
allowlistedDomains: string[];
allowlistedBundleIds: string[];
accessControl?: Partner["accessControl"] | null;
@@ -37,6 +38,7 @@ export function useAddPartner(
accessControl: params.accessControl ?? undefined,
allowlistedBundleIds: params.allowlistedBundleIds,
allowlistedDomains: params.allowlistedDomains,
imageUrl: params.imageUrl,
name: params.name,
// TODO - remove the requirement for permissions in API endpoint
permissions: ["FULL_CONTROL_V1"],
@@ -9,6 +9,7 @@ type UpdatePartnerParams = {
partnerId: string;
ecosystem: Ecosystem;
name: string;
imageUrl?: string | null;
allowlistedDomains: string[];
allowlistedBundleIds: string[];
accessControl?: {
@@ -43,6 +44,7 @@ export function useUpdatePartner(
accessControl: params.accessControl,
allowlistedBundleIds: params.allowlistedBundleIds,
allowlistedDomains: params.allowlistedDomains,
imageUrl: params.imageUrl,
name: params.name,
}),
@@ -46,11 +46,7 @@ export function DocLayout(props: DocLayoutProps) {
"hidden xl:flex",
)}
>
<DocSidebar
{...props.sideBar}
header={props.sidebarHeader}
className="pt-1"
/>
<DocSidebar {...props.sideBar} header={props.sidebarHeader} />
</aside>
)}
<div className="sticky top-sticky-top-height z-stickyMobileSidebar border-b bg-card/50 backdrop-blur-xl p-4 xl:hidden -mx-4">
+23 -5
View File
@@ -68,7 +68,11 @@ export function DocSidebar(props: ReferenceSideBarProps) {
{props.links.map((link, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: TODO - fix this
<li key={i}>
<SidebarItem link={link} onLinkClick={props.onLinkClick} />
<SidebarItem
link={link}
onLinkClick={props.onLinkClick}
isFirst={i === 0}
/>
</li>
))}
</ul>
@@ -76,7 +80,11 @@ export function DocSidebar(props: ReferenceSideBarProps) {
);
}
function SidebarItem(props: { link: SidebarLink; onLinkClick?: () => void }) {
function SidebarItem(props: {
link: SidebarLink;
onLinkClick?: () => void;
isFirst: boolean;
}) {
const pathname = usePathname();
if ("separator" in props.link) {
@@ -92,6 +100,7 @@ function SidebarItem(props: { link: SidebarLink; onLinkClick?: () => void }) {
if (link.isCollapsible === false) {
return (
<DocSidebarNonCollapsible
isFirst={props.isFirst}
key={link.name}
linkGroup={link}
onLinkClick={props.onLinkClick}
@@ -145,13 +154,14 @@ function SidebarItem(props: { link: SidebarLink; onLinkClick?: () => void }) {
function DocSidebarNonCollapsible(props: {
linkGroup: LinkGroup;
onLinkClick?: () => void;
isFirst: boolean;
}) {
const pathname = usePathname();
const { href, name, links, icon } = props.linkGroup;
const isCategoryActive = href ? isSamePage(pathname, href) : false;
return (
<div className="my-4">
<div className={cn("my-4", props.isFirst && "mt-0")}>
<div className="mb-2 flex items-center gap-2 rounded-lg text-foreground">
{icon && <SidebarIcon icon={icon} />}
{href ? (
@@ -173,7 +183,11 @@ function DocSidebarNonCollapsible(props: {
return (
// biome-ignore lint/suspicious/noArrayIndexKey: TODO - fix this
<li key={i}>
<SidebarItem link={link} onLinkClick={props.onLinkClick} />
<SidebarItem
link={link}
onLinkClick={props.onLinkClick}
isFirst={i === 0}
/>
</li>
);
})}
@@ -238,7 +252,11 @@ function DocSidebarCategory(props: {
return (
// biome-ignore lint/suspicious/noArrayIndexKey: TODO - fix this
<li key={i}>
<SidebarItem link={link} onLinkClick={props.onLinkClick} />
<SidebarItem
link={link}
onLinkClick={props.onLinkClick}
isFirst={i === 0}
/>
</li>
);
})}
+16
View File
@@ -1,5 +1,21 @@
# wagmi-inapp
## 0.0.52
### Patch Changes
- Updated dependencies [[`5352152`](https://github.com/thirdweb-dev/js/commit/53521528353a00691e82405460d4fb869ac986de)]:
- thirdweb@5.119.0
- @thirdweb-dev/wagmi-adapter@0.2.207
## 0.0.51
### Patch Changes
- Updated dependencies [[`1caf6dd`](https://github.com/thirdweb-dev/js/commit/1caf6dd4609ac72a06999ef0d70f31a371e5108a)]:
- thirdweb@5.118.2
- @thirdweb-dev/wagmi-adapter@0.2.206
## 0.0.50
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wagmi-inapp",
"private": true,
"version": "0.0.50",
"version": "0.0.52",
"type": "module",
"scripts": {
"dev": "vite",
+14
View File
@@ -1,5 +1,19 @@
# @thirdweb-dev/nebula
## 0.2.110
### Patch Changes
- Updated dependencies [[`5352152`](https://github.com/thirdweb-dev/js/commit/53521528353a00691e82405460d4fb869ac986de)]:
- thirdweb@5.119.0
## 0.2.109
### Patch Changes
- Updated dependencies [[`1caf6dd`](https://github.com/thirdweb-dev/js/commit/1caf6dd4609ac72a06999ef0d70f31a371e5108a)]:
- thirdweb@5.118.2
## 0.2.108
### Patch Changes
+1 -1
View File
@@ -57,5 +57,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.2.108"
"version": "0.2.110"
}
+12
View File
@@ -1,5 +1,17 @@
# thirdweb
## 5.119.0
### Minor Changes
- [#8686](https://github.com/thirdweb-dev/js/pull/8686) [`5352152`](https://github.com/thirdweb-dev/js/commit/53521528353a00691e82405460d4fb869ac986de) Thanks [@Yash094](https://github.com/Yash094)! - Add `requireApproval` option to `useConnectModal` hook to require terms of service and privacy policy acceptance before connecting an in-app wallet
## 5.118.2
### Patch Changes
- [#8671](https://github.com/thirdweb-dev/js/pull/8671) [`1caf6dd`](https://github.com/thirdweb-dev/js/commit/1caf6dd4609ac72a06999ef0d70f31a371e5108a) Thanks [@0xFirekeeper](https://github.com/0xFirekeeper)! - fix walletconnect not working on berachain and unallowlisted chains
## 5.118.1
### Patch Changes
+1 -1
View File
@@ -431,5 +431,5 @@
}
},
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "5.118.1"
"version": "5.119.0"
}
@@ -118,6 +118,7 @@ function Modal(
termsOfServiceUrl: props.termsOfServiceUrl,
title: props.title,
titleIconUrl: props.titleIcon,
requireApproval: props.requireApproval,
};
}, [
props.privacyPolicyUrl,
@@ -125,6 +126,7 @@ function Modal(
props.termsOfServiceUrl,
props.title,
props.titleIcon,
props.requireApproval,
]);
return (
@@ -449,6 +451,22 @@ export type UseConnectModalOptions = {
* Refer to the [`SiweAuthOptions`](https://portal.thirdweb.com/references/typescript/v5/SiweAuthOptions) for more details
*/
auth?: SiweAuthOptions;
/**
* Require terms of service and privacy policy to be accepted before connecting an in-app wallet.
*
* By default it's `false`
* @example
* ```tsx
* function Example() {
* const { connect } = useConnectModal();
* return <button onClick={() => connect({ client, requireApproval: true })}>
* Connect
* </button>
* }
* ```
*/
requireApproval?: boolean;
};
// TODO: consilidate Button/Embed/Modal props into one type with extras
@@ -110,14 +110,11 @@ export async function connectWC(
}
let optionalChains: Chain[] | undefined = wcOptions?.optionalChains;
let chainToRequest = options.chain;
const chainToRequest = options.chain;
// ignore the given options chains - and set the safe supported chains
if (walletId === "global.safe") {
optionalChains = chainsToRequestForSafe.map(getCachedChain);
if (chainToRequest && !optionalChains.includes(chainToRequest)) {
chainToRequest = undefined;
}
}
// For UniversalProvider, we still need chain configuration for session management
@@ -386,17 +383,6 @@ async function initProvider(
"@walletconnect/universal-provider"
);
let optionalChains: Chain[] | undefined = wcOptions?.optionalChains;
let chainToRequest = options.chain;
// ignore the given options chains - and set the safe supported chains
if (walletId === "global.safe") {
optionalChains = chainsToRequestForSafe.map(getCachedChain);
if (chainToRequest && !optionalChains.includes(chainToRequest)) {
chainToRequest = undefined;
}
}
const provider = await UniversalProvider.init({
metadata: {
description:
@@ -736,4 +722,5 @@ const chainsToRequestForSafe = [
1101, // Polygon zkEVM Mainnet
324, // zkSync Era mainnet
534352, // Scroll mainnet
80094, // Berachain mainnet
];
+4
View File
@@ -1,5 +1,9 @@
# @thirdweb-dev/wagmi-adapter
## 0.2.207
## 0.2.206
## 0.2.205
## 0.2.204
+1 -1
View File
@@ -56,5 +56,5 @@
"type": "module",
"types": "./dist/types/exports/thirdweb.d.ts",
"typings": "./dist/types/exports/thirdweb.d.ts",
"version": "0.2.205"
"version": "0.2.207"
}