1
1
'use client'
2
2
3
3
import { usePlan } from "@/hooks"
4
- import { useW3 , DID , Client , AccountDID } from "@w3ui/react"
4
+ import { useW3 , DID , Client , AccountDID , Account } from "@w3ui/react"
5
5
import { ArrowTopRightOnSquareIcon , CheckCircleIcon } from '@heroicons/react/24/outline'
6
6
import DefaultLoader from "@/components/Loader"
7
7
import { useState } from "react"
@@ -15,10 +15,11 @@ import * as Access from "@web3-storage/access/access"
15
15
import * as DidMailto from "@web3-storage/did-mailto"
16
16
import * as Ucanto from "@ucanto/core"
17
17
import { Plan } from "@web3-storage/capabilities"
18
- import { H2 } from "@/components/Text"
19
- import { Capability , Delegation } from "@ucanto/interface"
18
+ import { H1 , H2 } from "@/components/Text"
19
+ import { Ability , Capability , Delegation } from "@ucanto/interface"
20
20
21
21
interface PlanSectionProps {
22
+ account : Account
22
23
planID : DID
23
24
planName : string
24
25
flatFee : number
@@ -40,9 +41,8 @@ const planRanks: Record<string, number> = {
40
41
41
42
const buttonText = ( currentPlan : string , newPlan : string ) => ( planRanks [ currentPlan ] > planRanks [ newPlan ] ) ? 'Downgrade' : 'Upgrade'
42
43
43
- function PlanSection ( { planID, planName, flatFee, flatFeeAllotment, perGbFee } : PlanSectionProps ) {
44
- const [ { accounts } ] = useW3 ( )
45
- const { data : plan , setPlan, isLoading } = usePlan ( accounts [ 0 ] )
44
+ function PlanSection ( { account, planID, planName, flatFee, flatFeeAllotment, perGbFee } : PlanSectionProps ) {
45
+ const { data : plan , setPlan, isLoading } = usePlan ( account )
46
46
const currentPlanID = plan ?. product
47
47
const isCurrentPlan = currentPlanID === planID
48
48
const [ isUpdatingPlan , setIsUpdatingPlan ] = useState ( false )
@@ -84,12 +84,13 @@ function PlanSection ({ planID, planName, flatFee, flatFeeAllotment, perGbFee }:
84
84
< div > Additional at ${ perGbFee } /GB per month</ div >
85
85
</ div >
86
86
</ div >
87
+ < div className = 'flex-grow' > </ div >
87
88
{
88
89
( isLoading || isUpdatingPlan || ! currentPlanID ) ? (
89
90
< DefaultLoader className = 'h-6 w-6' />
90
91
) : (
91
92
isCurrentPlan ? (
92
- < div className = 'h-4 ' >
93
+ < div className = 'h-7 ' >
93
94
{ ( currentPlanID === planID ) && (
94
95
< h5 className = 'font-bold' > Current Plan</ h5 >
95
96
) }
@@ -114,46 +115,62 @@ function getCapabilities (delegations: Delegation[]): Capability[] {
114
115
}
115
116
}
116
117
117
- function findAccountsICanAdminister ( client : Client ) : AccountDID [ ] {
118
- return Array . from ( new Set (
119
- getCapabilities ( client . proofs ( [ { can : Plan . createAdminSession . can , with : 'ucan:*' } ] ) )
118
+ function doesCapabilityGrantAbility ( capability : Ability , ability : Ability ) {
119
+ if ( capability === ability ) {
120
+ return true
121
+ } else if ( capability . endsWith ( '/*' ) ) {
122
+ return ability . startsWith ( capability . slice ( 0 , - 1 ) )
123
+ } else {
124
+ return false
125
+ }
126
+ }
127
+
128
+ function findAccountResourcesWithCapability ( client : Client , ability : Ability ) : Set < AccountDID > {
129
+ return new Set (
130
+ getCapabilities ( client . proofs ( [ { can : ability , with : 'ucan:*' } ] ) )
120
131
. filter ( cap => {
121
- const hasCapability = cap . can === Plan . createAdminSession . can || cap . can === 'plan/*' || '*'
122
132
const isAccount = cap . with . startsWith ( 'did:mailto:' )
123
- return hasCapability && isAccount
133
+ return doesCapabilityGrantAbility ( cap . can , ability ) && isAccount
124
134
} )
125
135
. map ( cap => cap . with ) as AccountDID [ ]
126
- ) )
136
+ )
127
137
}
128
138
129
139
interface DelegationPlanCreateAdminSessionInput {
130
140
email : string
131
141
}
132
- function DelegatePlanCreateAdminSessionForm ( { className = '' } : { className ?: string } ) {
133
- const [ { client, accounts } ] = useW3 ( )
142
+
143
+ function DelegatePlanCreateAdminSessionForm ( { className = '' , account } : { className ?: string , account : Account } ) {
144
+ const [ { client } ] = useW3 ( )
134
145
135
146
const { register, handleSubmit } = useForm < DelegationPlanCreateAdminSessionInput > ( )
136
147
const onSubmit : SubmitHandler < DelegationPlanCreateAdminSessionInput > = async ( data ) => {
137
- const currentAccount = accounts [ 0 ]
138
- if ( client && currentAccount ) {
148
+ if ( client && account ) {
139
149
const email = data . email as `${string } @${string } `
150
+ const capabilities = [
151
+ {
152
+ with : account . did ( ) ,
153
+ can : Plan . createAdminSession . can
154
+ } ,
155
+ {
156
+ with : account . did ( ) ,
157
+ can : Plan . get . can
158
+ } ,
159
+ {
160
+ with : account . did ( ) ,
161
+ can : Plan . set . can
162
+ }
163
+ ]
140
164
await ucantoast ( Access . delegate ( client . agent , {
141
165
delegations : [
142
166
await delegate ( {
143
167
issuer : client . agent . issuer ,
144
168
audience : Ucanto . DID . parse ( DidMailto . fromEmail ( email ) ) ,
145
- capabilities : [
146
- {
147
- with : currentAccount . did ( ) ,
148
- can : Plan . createAdminSession . can
149
- }
150
- ] ,
169
+ // @ts -expect-error not sure why TS doesn't like this but I'm pretty sure it's safe to ignore
170
+ capabilities,
151
171
// TODO default to 1 year for now, but let's add UI for this soon
152
172
lifetimeInSeconds : 60 * 60 * 24 * 365 ,
153
- proofs : client . proofs ( [ {
154
- with : currentAccount . did ( ) ,
155
- can : Plan . createAdminSession . can
156
- } ] )
173
+ proofs : client . proofs ( capabilities )
157
174
} )
158
175
159
176
]
@@ -164,22 +181,12 @@ function DelegatePlanCreateAdminSessionForm ({ className = '' }: { className?: s
164
181
} )
165
182
}
166
183
}
167
- //console.log("PROOFS", client?.proofs().map(p => JSON.parse(JSON.stringify(p))))
168
-
169
- /*client?.proofs().reduce((m, delegation) => {
170
- const capabilities = delegation.capabilities.filter(cap => cap.can === '*' || cap.can === 'plan/*' || cap.can === 'plan/create-admin-session')
171
- if (capabilities.length > 0){
172
- console.log("CAPS", capabilities, JSON.parse(JSON.stringify(delegation)))
173
- }
174
- return m.concat()
175
- }, [])*/
176
-
177
184
return (
178
185
< form onSubmit = { handleSubmit ( onSubmit ) } className = { `flex flex-col space-y-2 ${ className } ` } >
179
186
< label className = 'w-full' >
180
- < H2 > Delegate billing admin portal access </ H2 >
187
+ < H2 > Delegate access to { DidMailto . toEmail ( account . did ( ) ) } 's < br /> billing admin portal</ H2 >
181
188
< input className = 'text-black py-2 px-2 rounded block w-full border border-gray-800'
182
- placeholder = 'Email' type = 'email'
189
+ placeholder = 'To Email' type = 'email'
183
190
{ ...register ( 'email' ) } />
184
191
</ label >
185
192
< input className = 'w3ui-button' type = 'submit' value = 'Delegate' />
@@ -197,13 +204,13 @@ function CustomerPortalLink ({ did }: { did: AccountDID }) {
197
204
< ArrowPathIcon className = { `h-5 w-5 text-white ${ generatingCustomerPortalLink ? 'animate-spin' : '' } ` } />
198
205
</ button >
199
206
< a className = 'w3ui-button' href = { customerPortalLink } target = "_blank" rel = "noopener noreferrer" >
200
- Open Customer Portal
207
+ Open Billing Portal
201
208
< ArrowTopRightOnSquareIcon className = 'relative inline h-5 w-4 ml-1 -mt-1' />
202
209
</ a >
203
210
</ div >
204
211
) : (
205
212
< button className = 'w3ui-button' onClick = { ( ) => generateCustomerPortalLink ( did ) } disabled = { generatingCustomerPortalLink } >
206
- Access Customer Portal for { DidMailto . toEmail ( did as DidMailto . DidMailto ) }
213
+ Generate Link
207
214
{ generatingCustomerPortalLink &&
208
215
< ArrowPathIcon className = 'inline ml-2 h-5 w-5 text-white animate-spin' />
209
216
}
@@ -227,7 +234,6 @@ function useCustomerPortalLink () {
227
234
console . debug ( `w3up account is ${ account } , could not generate customer portal link` )
228
235
} else {
229
236
setGeneratingCustomerPortalLink ( true )
230
- console . log ( "PROFOS" , JSON . parse ( JSON . stringify ( client . proofs ( [ { can : Plan . createAdminSession . can , with : did } ] ) ) ) )
231
237
const result = await account . plan . createAdminSession ( did , location . href )
232
238
setGeneratingCustomerPortalLink ( false )
233
239
if ( result . ok ) {
@@ -241,48 +247,47 @@ function useCustomerPortalLink () {
241
247
return { customerPortalLink, generateCustomerPortalLink, generatingCustomerPortalLink }
242
248
}
243
249
250
+ function AccountAdmin ( { account } : { account : Account } ) {
251
+ return (
252
+ < div className = 'flex flex-col space-y-2' >
253
+ < H1 > { DidMailto . toEmail ( account . did ( ) ) } </ H1 >
254
+ < div >
255
+ < H2 > Pick a Plan</ H2 >
256
+ < div className = 'flex flex-row xl:space-x-1' >
257
+ < PlanSection account = { account } planID = { PLANS [ 'starter' ] } planName = 'Starter' flatFee = { 0 } flatFeeAllotment = { 5 } perGbFee = { 0.15 } />
258
+ < PlanSection account = { account } planID = { PLANS [ 'lite' ] } planName = 'Lite' flatFee = { 10 } flatFeeAllotment = { 100 } perGbFee = { 0.05 } />
259
+ < PlanSection account = { account } planID = { PLANS [ 'business' ] } planName = 'Business' flatFee = { 100 } flatFeeAllotment = { 2000 } perGbFee = { 0.03 } />
260
+ </ div >
261
+ </ div >
262
+ < div >
263
+ < H2 > Access Billing Admin Portal</ H2 >
264
+ < CustomerPortalLink did = { account . did ( ) } />
265
+ </ div >
266
+ < DelegatePlanCreateAdminSessionForm account = { account } className = 'w-96' />
267
+ </ div >
268
+ )
269
+ }
270
+
244
271
function Plans ( ) {
245
272
const [ { client, accounts } ] = useW3 ( )
246
273
const account = accounts [ 0 ]
247
274
248
- const [ claimingDelegations , setClaimingDelegations ] = useState ( false )
249
- async function claimDelegations ( ) {
250
- try {
251
- setClaimingDelegations ( true )
252
- await client ?. capability . access . claim ( )
253
- await client ?. capability . access . claim ( { audience : account . did ( ) } )
254
- } finally {
255
- setClaimingDelegations ( false )
256
- }
257
- }
258
- const billingAdminAccounts : AccountDID [ ] = client ? findAccountsICanAdminister ( client ) : [ ]
259
- console . log ( billingAdminAccounts )
275
+ const billingAdminAccounts : Set < AccountDID > = client ? findAccountResourcesWithCapability ( client , Plan . createAdminSession . can ) : new Set ( )
276
+ const planAdminAccounts : Set < AccountDID > = client ? findAccountResourcesWithCapability ( client , Plan . set . can ) : new Set ( )
277
+ const adminableAccounts : AccountDID [ ] = Array . from ( new Set < AccountDID > ( [ ...billingAdminAccounts , ...planAdminAccounts ] ) )
278
+
260
279
return (
261
- < div className = 'py-8 flex flex-col items-center space-y-8' >
262
- < div className = 'flex flex-col items-center' >
263
- < h1 className = 'text-2xl font-mono mb-8 font-bold' > Billing</ h1 >
264
- < CustomerPortalLink did = { account . did ( ) } />
265
- < DelegatePlanCreateAdminSessionForm className = 'my-4' />
266
- < div >
267
- { /*<button className='w3ui-button' onClick={claimDelegations} disabled={claimingDelegations}>
268
- Check For Billing Admin Delegations
269
- {claimingDelegations &&
270
- <ArrowPathIcon className='inline ml-2 h-5 w-5 text-white animate-spin' />
271
- }
272
- </button>
273
- */ }
274
- { billingAdminAccounts . map ( did => ( < CustomerPortalLink did = { did } key = { did } /> ) ) }
275
- </ div >
276
- </ div >
277
- < div className = 'flex flex-col items-center' >
278
- < h1 className = 'text-2xl font-mono mb-8 font-bold' > Plans</ h1 >
279
- < p className = 'mb-4' > Pick the price plan that works for you.</ p >
280
- < div className = 'flex flex-col space-y-2 xl:flex-row xl:space-y-0 xl:space-x-2' >
281
- < PlanSection planID = { PLANS [ 'starter' ] } planName = 'Starter' flatFee = { 0 } flatFeeAllotment = { 5 } perGbFee = { 0.15 } />
282
- < PlanSection planID = { PLANS [ 'lite' ] } planName = 'Lite' flatFee = { 10 } flatFeeAllotment = { 100 } perGbFee = { 0.05 } />
283
- < PlanSection planID = { PLANS [ 'business' ] } planName = 'Business' flatFee = { 100 } flatFeeAllotment = { 2000 } perGbFee = { 0.03 } />
284
- </ div >
285
- </ div >
280
+ < div className = 'py-8 flex flex-col space-y-12' >
281
+ < h1 className = 'text-2xl font-mono font-bold' > Billing</ h1 >
282
+ < AccountAdmin account = { account } />
283
+ { adminableAccounts . map ( did => ( client && ( did !== account . did ( ) ) ? (
284
+ < AccountAdmin key = { did }
285
+ account = { new Account ( {
286
+ id : did as DidMailto . DidMailto ,
287
+ agent : client . agent ,
288
+ proofs : [ ]
289
+ } ) } />
290
+ ) : null ) ) }
286
291
</ div >
287
292
)
288
293
}
0 commit comments