Skip to content

Commit 433433d

Browse files
committed
chore: merge develop into release for v1.1060.0
2 parents b018b9b + 0be7d38 commit 433433d

56 files changed

Lines changed: 1268 additions & 317 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.env‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ VITE_FEATURE_EARN_TAB=true
112112
VITE_FEATURE_ACROSS_SWAP=true
113113
VITE_FEATURE_DEBRIDGE_SWAP=true
114114
VITE_FEATURE_BOB_GATEWAY_SWAP=true
115-
VITE_FEATURE_SYMBIOSIS_SWAP=false
115+
VITE_FEATURE_SYMBIOSIS_SWAP=true
116116
VITE_FEATURE_USERBACK=true
117117
VITE_FEATURE_AGENTIC_CHAT=false
118118
VITE_FEATURE_MM_NATIVE_MULTICHAIN=false

‎.env.development‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ NODE_ENV=development
33
# feature flags
44
VITE_FEATURE_CHAINFLIP_LENDING=true
55
VITE_FEATURE_THORCHAIN_TCY_ACTIVITY=true
6-
VITE_FEATURE_SYMBIOSIS_SWAP=true
76
VITE_FEATURE_TX_HISTORY_BYE_BYE=true
87
VITE_FEATURE_WC_DIRECT_CONNECTION=true
98
VITE_FEATURE_MANTLE=true

‎docs/affiliates.md‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ applies your configured fee automatically.
1414

1515
Register at the [Affiliate Dashboard](https://dashboard.affiliate.shapeshift.com/): connect your wallet, sign
1616
in (a wallet signature — no gas), then choose your partner code and fee. The code is mapped to your
17-
wallet, and you'll use it everywhere below.
17+
wallet, and you'll use it everywhere below. The registering wallet owns the affiliate; from the
18+
dashboard's Settings tab it can grant other wallets admin access, and each admin signs in with its own
19+
wallet to manage the same configuration.
1820

1921
### 2. Using the Swap Widget
2022

@@ -94,9 +96,13 @@ curl "https://api.shapeshift.com/v1/affiliate/swaps?partnerCode=your-partner-cod
9496
| GET | `/v1/affiliate/{address}` | none | Look up an affiliate by wallet address |
9597
| POST | `/v1/affiliate` | wallet (SIWE) | Register as an affiliate |
9698
| PATCH | `/v1/affiliate/{address}` | wallet (SIWE) | Update affiliate settings |
99+
| GET | `/v1/affiliate/{address}/members` | wallet (SIWE) | List the owner and admin wallets |
100+
| POST | `/v1/affiliate/{address}/members` | wallet (SIWE) | Grant a wallet admin access |
101+
| DELETE | `/v1/affiliate/{address}/members/{memberAddress}` | wallet (SIWE) | Revoke a wallet's admin access |
97102

98103
Wallet-authenticated endpoints use Sign-In With Ethereum (`POST /v1/auth/siwe/nonce` →
99-
`POST /v1/auth/siwe/verify`). Most partners never call these directly — the
104+
`POST /v1/auth/siwe/verify`), and `{address}` is always the signed-in wallet, whether it is the owner
105+
or an admin. Most partners never call these directly — the
100106
[Affiliate Dashboard](https://dashboard.affiliate.shapeshift.com/) handles registration for you.
101107

102108
See the full request/response schemas in the [Public API reference](https://api.shapeshift.com/docs).

‎packages/affiliate-dashboard/src/App.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export const App = (): React.JSX.Element => {
105105
affiliateAddress={affiliateAddress}
106106
config={configQuery.data}
107107
actions={actions}
108+
authHeaders={authHeaders}
108109
isAuthenticated={isAuthenticated}
109110
isAuthenticating={isAuthenticating}
110111
authError={authError}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import {
2+
AlertDialog,
3+
AlertDialogBody,
4+
AlertDialogContent,
5+
AlertDialogFooter,
6+
AlertDialogHeader,
7+
AlertDialogOverlay,
8+
Badge,
9+
Box,
10+
Button,
11+
CloseButton,
12+
Flex,
13+
HStack,
14+
Input,
15+
Stack,
16+
Text,
17+
} from '@chakra-ui/react'
18+
import { useRef, useState } from 'react'
19+
20+
import type { ActionMessage } from '../../hooks/useAffiliateActions'
21+
import { useAffiliateMembers } from '../../hooks/useAffiliateMembers'
22+
import { EVM_ADDRESS_REGEX, formatDate } from '../../lib/format'
23+
import { SettingsCard } from './SettingsCard'
24+
25+
interface AccessManagementCardProps {
26+
affiliateAddress: string
27+
authHeaders: Record<string, string>
28+
setMessage: (message: ActionMessage | null) => void
29+
}
30+
31+
export const AccessManagementCard = ({
32+
affiliateAddress,
33+
authHeaders,
34+
setMessage,
35+
}: AccessManagementCardProps): React.JSX.Element => {
36+
const [address, setAddress] = useState('')
37+
const [pendingRemoval, setPendingRemoval] = useState<string | null>(null)
38+
const cancelRef = useRef<HTMLButtonElement>(null)
39+
const { query, isAdding, isRemoving, addMember, removeMember } = useAffiliateMembers({
40+
affiliateAddress,
41+
authHeaders,
42+
setMessage,
43+
})
44+
45+
const handleAdd = async (): Promise<void> => {
46+
const trimmed = address.trim()
47+
if (!EVM_ADDRESS_REGEX.test(trimmed)) {
48+
setMessage({ type: 'error', text: 'Invalid EVM address' })
49+
return
50+
}
51+
try {
52+
await addMember(trimmed)
53+
setAddress('')
54+
} catch {
55+
// error surfaced via useAffiliateMembers.onError; preserve input for retry
56+
}
57+
}
58+
59+
const handleRemove = async (): Promise<void> => {
60+
if (!pendingRemoval) return
61+
try {
62+
await removeMember(pendingRemoval)
63+
} catch {
64+
// error surfaced via useAffiliateMembers.onError
65+
} finally {
66+
setPendingRemoval(null)
67+
}
68+
}
69+
70+
return (
71+
<SettingsCard
72+
title='Authorized Wallets'
73+
description='Wallets that can sign in and manage this affiliate.'
74+
>
75+
<Stack spacing={4}>
76+
{query.isLoading && (
77+
<Text fontSize='sm' color='fg.muted'>
78+
Loading...
79+
</Text>
80+
)}
81+
{query.error && (
82+
<Text fontSize='sm' color='danger'>
83+
{query.error.message}
84+
</Text>
85+
)}
86+
<Stack spacing={0}>
87+
{(query.data ?? []).map(member => {
88+
const isSelf = member.walletAddress.toLowerCase() === affiliateAddress.toLowerCase()
89+
const canRemove = member.role === 'admin' && !isSelf
90+
return (
91+
<Flex
92+
key={member.walletAddress}
93+
justify='space-between'
94+
align='center'
95+
gap={4}
96+
py={2}
97+
borderBottom='1px solid'
98+
borderColor='border.muted'
99+
_last={{ borderBottom: 'none' }}
100+
>
101+
<HStack spacing={3} minW={0}>
102+
<Text
103+
fontSize='sm'
104+
fontWeight={500}
105+
color='fg.bright'
106+
fontFamily='mono'
107+
wordBreak='break-all'
108+
>
109+
{member.walletAddress}
110+
</Text>
111+
<Badge
112+
variant='subtle'
113+
colorScheme={member.role === 'owner' ? 'blue' : 'gray'}
114+
textTransform='capitalize'
115+
flexShrink={0}
116+
>
117+
{member.role}
118+
</Badge>
119+
</HStack>
120+
<HStack spacing={3} flexShrink={0}>
121+
<Text fontSize='xs' color='fg.muted' display={{ base: 'none', md: 'block' }}>
122+
{member.role === 'owner' ? 'Registered' : 'Added'}{' '}
123+
{formatDate(member.createdAt)}
124+
</Text>
125+
<Box w={6} h={6} flexShrink={0}>
126+
{canRemove && (
127+
<CloseButton
128+
size='sm'
129+
aria-label={`Remove ${member.walletAddress}`}
130+
isDisabled={isRemoving}
131+
onClick={() => setPendingRemoval(member.walletAddress)}
132+
/>
133+
)}
134+
</Box>
135+
</HStack>
136+
</Flex>
137+
)
138+
})}
139+
</Stack>
140+
<HStack spacing={3} wrap='wrap'>
141+
<Input
142+
value={address}
143+
onChange={e => setAddress(e.target.value)}
144+
placeholder='Wallet address'
145+
spellCheck={false}
146+
w='46ch'
147+
maxW='100%'
148+
/>
149+
<Button
150+
onClick={() => void handleAdd()}
151+
isDisabled={!address.trim()}
152+
isLoading={isAdding}
153+
loadingText='Adding...'
154+
>
155+
Add admin
156+
</Button>
157+
</HStack>
158+
</Stack>
159+
<AlertDialog
160+
isOpen={pendingRemoval !== null}
161+
leastDestructiveRef={cancelRef}
162+
onClose={() => setPendingRemoval(null)}
163+
isCentered
164+
>
165+
<AlertDialogOverlay>
166+
<AlertDialogContent bg='bg.surface' border='1px solid' borderColor='border.subtle'>
167+
<AlertDialogHeader fontSize='md' fontWeight={600} color='fg.bright'>
168+
Remove admin
169+
</AlertDialogHeader>
170+
<AlertDialogBody fontSize='sm' color='fg.muted'>
171+
This wallet will no longer be able to sign in and manage this affiliate.
172+
<Text mt={3} fontFamily='mono' color='fg.bright' wordBreak='break-all'>
173+
{pendingRemoval}
174+
</Text>
175+
</AlertDialogBody>
176+
<AlertDialogFooter gap={3}>
177+
<Button
178+
ref={cancelRef}
179+
variant='outline'
180+
colorScheme='gray'
181+
borderColor='border.input'
182+
color='fg.default'
183+
onClick={() => setPendingRemoval(null)}
184+
>
185+
Cancel
186+
</Button>
187+
<Button
188+
bg='red.700'
189+
color='white'
190+
_hover={{ bg: 'red.800' }}
191+
_active={{ bg: 'red.900' }}
192+
isLoading={isRemoving}
193+
onClick={() => void handleRemove()}
194+
>
195+
Remove
196+
</Button>
197+
</AlertDialogFooter>
198+
</AlertDialogContent>
199+
</AlertDialogOverlay>
200+
</AlertDialog>
201+
</SettingsCard>
202+
)
203+
}

‎packages/affiliate-dashboard/src/components/settings/AuthStatusBar.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const AuthStatusBar = ({ onSignOut }: AuthStatusBarProps): React.JSX.Elem
1919
Authenticated
2020
</Heading>
2121
<Text fontSize='sm' color='fg.muted' lineHeight={1.5}>
22-
You&apos;re signed in and can manage settings for this wallet.
22+
You&apos;re signed in and can manage this affiliate configuration.
2323
</Text>
2424
</Stack>
2525
<Button

‎packages/affiliate-dashboard/src/components/settings/ConfigSummaryCard.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ const Row = ({
4747
export const ConfigSummaryCard = ({ config }: ConfigSummaryCardProps): React.JSX.Element => (
4848
<SettingsCard title='Current Configuration'>
4949
<Stack spacing={0}>
50-
<Row label='Wallet' mono>
50+
<Row label='Owner Wallet' mono>
5151
{config.walletAddress}
5252
</Row>
53+
{config.role && <Row label='Your Role'>{config.role === 'owner' ? 'Owner' : 'Admin'}</Row>}
5354
<Row label='Receive Address' mono>
5455
{config.receiveAddress ?? config.walletAddress}
5556
</Row>

‎packages/affiliate-dashboard/src/components/settings/ReceiveAddressCard.tsx‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useState } from 'react'
33

44
import type { ActionMessage } from '../../hooks/useAffiliateActions'
55
import type { AffiliateConfig } from '../../hooks/useAffiliateConfig'
6+
import { EVM_ADDRESS_REGEX } from '../../lib/format'
67
import { SettingsCard } from './SettingsCard'
78

89
interface ReceiveAddressCardProps {
@@ -12,8 +13,6 @@ interface ReceiveAddressCardProps {
1213
onValidationError: (message: ActionMessage) => void
1314
}
1415

15-
const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/
16-
1716
export const ReceiveAddressCard = ({
1817
config,
1918
isLoading,
@@ -27,7 +26,7 @@ export const ReceiveAddressCard = ({
2726

2827
const handleUpdate = async (): Promise<void> => {
2928
const trimmed = address.trim()
30-
if (!ADDRESS_REGEX.test(trimmed)) {
29+
if (!EVM_ADDRESS_REGEX.test(trimmed)) {
3130
onValidationError({ type: 'error', text: 'Invalid EVM address' })
3231
return
3332
}
@@ -42,7 +41,7 @@ export const ReceiveAddressCard = ({
4241
return (
4342
<SettingsCard
4443
title='Receive Address'
45-
description='Destination for affiliate revenue. Defaults to your connected wallet.'
44+
description='Destination for affiliate revenue. Defaults to the owner wallet.'
4645
>
4746
<HStack spacing={3} wrap='wrap'>
4847
<Input

‎packages/affiliate-dashboard/src/components/settings/SettingsTab.tsx‎

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
useAffiliateActions,
66
} from '../../hooks/useAffiliateActions'
77
import type { AffiliateConfig } from '../../hooks/useAffiliateConfig'
8+
import { AccessManagementCard } from './AccessManagementCard'
89
import { ActionMessage } from './ActionMessage'
910
import { AffiliateBpsCard } from './AffiliateBpsCard'
1011
import { AuthBanner } from './AuthBanner'
@@ -17,6 +18,7 @@ interface SettingsTabProps {
1718
affiliateAddress: string
1819
config: AffiliateConfig | undefined
1920
actions: ReturnType<typeof useAffiliateActions>
21+
authHeaders: Record<string, string>
2022
isAuthenticated: boolean
2123
isAuthenticating: boolean
2224
authError: string | null
@@ -29,6 +31,7 @@ export const SettingsTab = ({
2931
affiliateAddress,
3032
config,
3133
actions,
34+
authHeaders,
3235
isAuthenticated,
3336
isAuthenticating,
3437
authError,
@@ -60,20 +63,25 @@ export const SettingsTab = ({
6063
<>
6164
<ConfigSummaryCard config={config} />
6265
{isAuthenticated && (
63-
<AffiliateBpsCard
64-
currentBps={config.partnerBps}
65-
isLoading={actions.isLoading}
66-
onUpdate={bps => actions.updateBps(bps)}
67-
onValidationError={onValidationError}
68-
/>
69-
)}
70-
{isAuthenticated && (
71-
<ReceiveAddressCard
72-
config={config}
73-
isLoading={actions.isLoading}
74-
onUpdate={addr => actions.updateReceiveAddress(addr)}
75-
onValidationError={onValidationError}
76-
/>
66+
<>
67+
<AffiliateBpsCard
68+
currentBps={config.partnerBps}
69+
isLoading={actions.isLoading}
70+
onUpdate={bps => actions.updateBps(bps)}
71+
onValidationError={onValidationError}
72+
/>
73+
<ReceiveAddressCard
74+
config={config}
75+
isLoading={actions.isLoading}
76+
onUpdate={addr => actions.updateReceiveAddress(addr)}
77+
onValidationError={onValidationError}
78+
/>
79+
<AccessManagementCard
80+
affiliateAddress={affiliateAddress}
81+
authHeaders={authHeaders}
82+
setMessage={actions.setMessage}
83+
/>
84+
</>
7785
)}
7886
</>
7987
)}

‎packages/affiliate-dashboard/src/hooks/useAffiliateConfig.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { z } from 'zod'
55
import { parseResponse } from '../lib/api'
66
import { AFFILIATE_URL } from '../lib/constants'
77

8+
export const AffiliateRoleSchema = z.enum(['owner', 'admin'])
9+
810
const AffiliateConfigSchema = z.object({
911
id: z.string(),
1012
walletAddress: z.string(),
@@ -15,6 +17,7 @@ const AffiliateConfigSchema = z.object({
1517
isActive: z.boolean(),
1618
createdAt: z.string().datetime(),
1719
updatedAt: z.string().datetime(),
20+
role: AffiliateRoleSchema.optional(),
1821
})
1922

2023
export type AffiliateConfig = z.infer<typeof AffiliateConfigSchema>

0 commit comments

Comments
 (0)