22
33import type { Project } from "@/api/projects" ;
44import type { SMSCountryTiers } from "@/api/sms" ;
5+ import type { Team } from "@/api/team" ;
56import { DynamicHeight } from "@/components/ui/DynamicHeight" ;
67import { Spinner } from "@/components/ui/Spinner/Spinner" ;
78import { UnderlineLink } from "@/components/ui/UnderlineLink" ;
@@ -20,6 +21,8 @@ import { Input } from "@/components/ui/input";
2021import { Label } from "@/components/ui/label" ;
2122import { Textarea } from "@/components/ui/textarea" ;
2223import { TrackedLinkTW } from "@/components/ui/tracked-link" ;
24+ import { useThirdwebClient } from "@/constants/thirdweb.client" ;
25+ import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler" ;
2326import { cn } from "@/lib/utils" ;
2427import { updateProjectClient } from "@3rdweb-sdk/react/hooks/useApi" ;
2528import { zodResolver } from "@hookform/resolvers/zod" ;
@@ -33,10 +36,12 @@ import {
3336import { useTrack } from "hooks/analytics/useTrack" ;
3437import { CircleAlertIcon , PlusIcon , Trash2Icon } from "lucide-react" ;
3538import type React from "react" ;
39+ import { useState } from "react" ;
3640import { type UseFormReturn , useFieldArray , useForm } from "react-hook-form" ;
3741import { toast } from "sonner" ;
42+ import { upload } from "thirdweb/storage" ;
3843import { toArrFromList } from "utils/string" ;
39- import type { Team } from "../../../@/api/team " ;
44+ import { FileInput } from "../../shared/FileInput " ;
4045import CountrySelector from "./sms-country-select/country-selector" ;
4146
4247type InAppWalletSettingsPageProps = {
@@ -339,41 +344,49 @@ function BrandingFieldset(props: {
339344 className = "grid grid-cols-1 gap-6 lg:grid-cols-2"
340345 show = { canEditAdvancedFeatures && ! ! form . watch ( "branding" ) }
341346 >
342- { /* Application Name */ }
347+ { /* Application Image */ }
343348 < FormField
344349 control = { form . control }
345- name = "branding.applicationName"
346- render = { ( { field } ) => (
347- < FormItem >
348- < FormLabel > Application Name</ FormLabel >
350+ name = "branding.applicationImageUrl"
351+ render = { ( ) => (
352+ < FormItem className = "space-y-1" >
353+ < FormLabel > Application Image URL</ FormLabel >
354+ < FormDescription className = "!mb-4" >
355+ Logo that will display in the emails sent to users.{ " " }
356+ < br className = "max-sm:hidden" /> The image must be squared with
357+ recommended size of 72x72 px.
358+ </ FormDescription >
349359 < FormControl >
350- < Input { ...field } />
360+ < AppImageFormControl
361+ uri = { form . watch ( "branding.applicationImageUrl" ) }
362+ setUri = { ( uri ) => {
363+ form . setValue ( "branding.applicationImageUrl" , uri , {
364+ shouldDirty : true ,
365+ shouldTouch : true ,
366+ } ) ;
367+ } }
368+ />
351369 </ FormControl >
352- < FormDescription >
353- Name that will be displayed in the emails sent to users.{ " " }
354- < br className = "max-sm:hidden" /> Defaults to your API Key's
355- name.
356- </ FormDescription >
357370 < FormMessage />
358371 </ FormItem >
359372 ) }
360373 />
361374
362- { /* Application Image */ }
375+ { /* Application Name */ }
363376 < FormField
364377 control = { form . control }
365- name = "branding.applicationImageUrl "
378+ name = "branding.applicationName "
366379 render = { ( { field } ) => (
367380 < FormItem >
368- < FormLabel > Application Image URL</ FormLabel >
381+ < FormLabel > Application Name</ FormLabel >
382+ < FormDescription className = "!mb-2" >
383+ Name that will be displayed in the emails sent to users.{ " " }
384+ < br className = "max-sm:hidden" /> Defaults to your API Key's
385+ name.
386+ </ FormDescription >
369387 < FormControl >
370388 < Input { ...field } />
371389 </ FormControl >
372- < FormDescription >
373- Logo that will display in the emails sent to users.{ " " }
374- < br className = "max-sm:hidden" /> The image must be squared with
375- recommended size of 72x72 px.
376- </ FormDescription >
377390 < FormMessage />
378391 </ FormItem >
379392 ) }
@@ -383,6 +396,61 @@ function BrandingFieldset(props: {
383396 ) ;
384397}
385398
399+ function AppImageFormControl ( props : {
400+ uri : string | undefined ;
401+ setUri : ( uri : string ) => void ;
402+ } ) {
403+ const client = useThirdwebClient ( ) ;
404+ const [ image , setImage ] = useState < File | undefined > ( ) ;
405+ const resolveUrl = resolveSchemeWithErrorHandler ( {
406+ client : client ,
407+ uri : props . uri || undefined ,
408+ } ) ;
409+
410+ const uploadImage = useMutation ( {
411+ mutationFn : async ( file : File ) => {
412+ const uri = await upload ( {
413+ client : client ,
414+ files : [ file ] ,
415+ } ) ;
416+
417+ return uri ;
418+ } ,
419+ } ) ;
420+
421+ return (
422+ < div className = "flex" >
423+ < div className = "relative" >
424+ < FileInput
425+ accept = { { "image/*" : [ ] } }
426+ value = { image }
427+ setValue = { async ( v ) => {
428+ try {
429+ setImage ( v ) ;
430+ const uri = await uploadImage . mutateAsync ( v ) ;
431+ props . setUri ( uri ) ;
432+ } catch ( error ) {
433+ setImage ( undefined ) ;
434+ toast . error ( "Failed to upload image" , {
435+ description : error instanceof Error ? error . message : undefined ,
436+ } ) ;
437+ }
438+ } }
439+ className = "w-24 rounded-full bg-background lg:w-28"
440+ disableHelperText
441+ fileUrl = { resolveUrl }
442+ />
443+
444+ { uploadImage . isPending && (
445+ < div className = "absolute inset-0 flex items-center justify-center rounded-full border bg-background/50" >
446+ < Spinner className = "size-7" />
447+ </ div >
448+ ) }
449+ </ div >
450+ </ div >
451+ ) ;
452+ }
453+
386454function SMSCountryFields ( props : {
387455 form : UseFormReturn < ApiKeyEmbeddedWalletsValidationSchema > ;
388456 canEditAdvancedFeatures : boolean ;
0 commit comments