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

fix: go to service Button #630

Merged
merged 1 commit into from
Nov 25, 2024
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 app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ module.exports = {
// Display alert banner for the developer preview deployment
showPreviewAlert: process.env.NEXT_PUBLIC_SHOW_PREVIEW_ALERT || 'false',

contractingProvider: {
enable: true,
endpoint: 'https://contracting.demo.delta-dao.com'
},

networkAlertConfig: {
// Refresh interval for network status - 30 sec
refreshInterval: 30000,
Expand Down
4 changes: 4 additions & 0 deletions src/@context/MarketMetadata/_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export interface AppConfig {
roughTxGasEstimate: number
}
showPreviewAlert: string
contractingProvider: {
enable: boolean
endpoint: string
}
networkAlertConfig: {
// Refresh interval for network status - 30 sec
refreshInterval: number
Expand Down
15 changes: 12 additions & 3 deletions src/components/Asset/AssetActions/ButtonBuy/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { FormEvent, ReactElement } from 'react'
import Button from '../../../@shared/atoms/Button'
import styles from './index.module.css'
import Loader from '../../../@shared/atoms/Loader'
import { PAYMENT_MODES, PaymentMode } from '../Download/ContractingProvider'
import styles from './index.module.css'

export interface ButtonBuyProps {
action: 'download' | 'compute'
Expand Down Expand Up @@ -32,6 +33,7 @@ export interface ButtonBuyProps {
isAccountConnected?: boolean
hasProviderFee?: boolean
retry?: boolean
paymentMode?: PaymentMode
}

function getConsumeHelpText(
Expand Down Expand Up @@ -166,6 +168,7 @@ export default function ButtonBuy({
dtSymbol,
dtBalance,
assetType,
paymentMode,
assetTimeout,
isConsumable,
consumableFeedback,
Expand All @@ -191,13 +194,19 @@ export default function ButtonBuy({
? 'Retry'
: action === 'download'
? hasPreviousOrder && assetType === 'saas'
? 'Go to service'
? paymentMode === PAYMENT_MODES.PAYPERUSE
? `Buy access credit`
: 'Go to service'
: hasPreviousOrder
? 'Download'
: priceType === 'free'
? 'Get'
: assetType === 'saas'
? `Subscribe ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}`
? paymentMode === PAYMENT_MODES.PAYPERUSE
? `Buy access credit`
: `Subscribe ${
assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`
}`
: `Buy ${assetTimeout === 'Forever' ? '' : ` for ${assetTimeout}`}`
: hasPreviousOrder &&
hasPreviousOrderSelectedComputeAsset &&
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.container {
display: flex;
flex-direction: column;
margin-top: calc(var(--spacer) / 1.5);
margin-bottom: -1rem;
margin-left: calc(var(--spacer) / -1.5);
margin-right: calc(var(--spacer) / -1.5);
padding: calc(var(--spacer) / 1.5);
border-top: 1px solid var(--border-color);
}

.help {
composes: help from '../../ButtonBuy/index.module.css';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { ReactElement, useCallback, useEffect, useState } from 'react'
import styles from './index.module.css'
import Button from '../../../../@shared/atoms/Button'
import { useAccount, useSignMessage } from 'wagmi'
import { getContractingProviderNonce, getPayPerUseCount } from '../../utils'
import Alert from '../../../../@shared/atoms/Alert'
import { useMarketMetadata } from '../../../../../@context/MarketMetadata'
import { useAutomation } from '../../../../../@context/Automation/AutomationProvider'

export enum PAYMENT_MODES {
SUBSCRIPTION = 'subscription',
PAYPERUSE = 'payperuse'
}

export type PaymentMode = `${PAYMENT_MODES}`

export default function ContractingProvider(props: {
did: string
}): ReactElement {
const { did } = props
const { address } = useAccount()
const [isRequesting, setIsRequesting] = useState(false)
const [accessCreditsCount, setAccessCreditsCount] = useState<number>()
const {
signMessage,
data: signMessageData,
isSuccess,
isError
} = useSignMessage()
const {
appConfig: {
contractingProvider: { endpoint: contractingProviderEndpoint }
}
} = useMarketMetadata()

const { autoWallet, isAutomationEnabled } = useAutomation()

const [activeAddress, setActiveAddress] = useState<string>()
const [signature, setSignature] = useState<string>()

useEffect(() => {
if (isAutomationEnabled) setActiveAddress(autoWallet.address)
else setActiveAddress(address)
}, [address, autoWallet?.address, isAutomationEnabled])

const checkAccessCredits = async () => {
setIsRequesting(true)

const nonce = await getContractingProviderNonce(
contractingProviderEndpoint,
activeAddress
)
if (isAutomationEnabled) {
try {
const autoWalletSignature = await autoWallet.signMessage(nonce)
setSignature(autoWalletSignature)
} catch (e) {
setIsRequesting(false)
console.error(e)
}
} else {
signMessage({ message: nonce })
}
}

const updateCount = useCallback(async () => {
const count = await getPayPerUseCount(
contractingProviderEndpoint,
activeAddress,
signature,
did
)
setAccessCreditsCount(count)
setIsRequesting(false)
}, [contractingProviderEndpoint, activeAddress, signature, did])

useEffect(() => {
if (isError) setIsRequesting(false)
if (isSuccess) {
setSignature(signMessageData)
}
}, [isSuccess, isError])

useEffect(() => {
if (!signature) return
updateCount()
}, [signature])

return (
<div className={styles.container}>
{accessCreditsCount ? (
<Alert
state="info"
text={`You purchased access to this service **${accessCreditsCount} time${
accessCreditsCount > 1 ? 's' : ''
}**`}
action={{
name: 'Re-run',
handleAction: (e) => {
setAccessCreditsCount(undefined) // force visible re-render
e.preventDefault()
checkAccessCredits()
}
}}
/>
) : (
<Button
style="text"
onClick={(e) => {
e.preventDefault()
checkAccessCredits()
}}
disabled={isRequesting}
>
Check Access Credits
</Button>
)}
<div className={styles.help}>
You can validate your purchase count of this SaaS Offering against a
contracting provider instance.
</div>
</div>
)
}
64 changes: 56 additions & 8 deletions src/components/Asset/AssetActions/Download/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ import WhitelistIndicator from '../Compute/WhitelistIndicator'
import { Signer } from 'ethers'
import SuccessConfetti from '@components/@shared/SuccessConfetti'
import Input from '@components/@shared/FormInput'
import ContractingProvider, { PAYMENT_MODES } from './ContractingProvider'
import Button from '@components/@shared/atoms/Button'

export default function Download({
accountId,
Expand All @@ -61,7 +63,12 @@ export default function Download({
}): ReactElement {
const { isConnected } = useAccount()
const { isSupportedOceanNetwork } = useNetworkMetadata()
const { getOpcFeeForToken } = useMarketMetadata()
const {
getOpcFeeForToken,
appConfig: {
contractingProvider: { enable: isContractingFeatureEnabled }
}
} = useMarketMetadata()
const { isInPurgatory, isAssetNetwork } = useAsset()
const isMounted = useIsMounted()

Expand Down Expand Up @@ -171,16 +178,21 @@ export default function Download({
isAccountIdWhitelisted
])

function redirectToSaasUrl() {
window.open(asset.metadata.additionalInformation.saas.redirectUrl, '_blank')
}

async function handleOrderOrDownload(dataParams?: UserCustomParameters) {
setIsLoading(true)
setRetry(false)
try {
if (isOwned) {
if (
isOwned &&
asset?.metadata?.additionalInformation?.saas?.paymentMode !==
PAYMENT_MODES.PAYPERUSE
) {
if (asset?.metadata?.additionalInformation?.saas?.redirectUrl) {
window.open(
asset.metadata.additionalInformation.saas.redirectUrl,
'_blank'
)
redirectToSaasUrl()
setIsLoading(false)
return
}
Expand Down Expand Up @@ -236,12 +248,22 @@ export default function Download({
dtSymbol={asset?.datatokens[0]?.symbol}
dtBalance={dtBalance}
type="submit"
assetTimeout={secondsToString(asset?.services?.[0]?.timeout)}
assetTimeout={
asset?.metadata?.additionalInformation?.saas?.paymentMode ===
PAYMENT_MODES.PAYPERUSE
? // we dont have a timeout on payperuse
// as this is handled by service operators utilizing contracting provider
secondsToString(0)
: secondsToString(asset?.services?.[0]?.timeout)
}
assetType={
asset?.metadata?.additionalInformation?.saas
? 'saas'
: asset?.metadata?.type
}
paymentMode={
asset?.metadata?.additionalInformation?.saas?.paymentMode ?? undefined
}
stepText={statusText}
isLoading={isLoading}
priceType={asset.accessDetails?.type}
Expand Down Expand Up @@ -284,7 +306,28 @@ export default function Download({
size="large"
/>
)}
{!isInPurgatory && <PurchaseButton isValid={isValid} />}
{!isInPurgatory && (
<>
{asset?.metadata?.additionalInformation?.saas
?.paymentMode === PAYMENT_MODES.PAYPERUSE &&
asset?.metadata?.additionalInformation?.saas
?.redirectUrl && (
<div className={styles.payPerUseBtn}>
<Button
style="primary"
onClick={(e) => {
e.preventDefault()
redirectToSaasUrl()
}}
disabled={!isValid}
>
Go to service
</Button>
</div>
)}
<PurchaseButton isValid={isValid} />
</>
)}
<Field
component={Input}
name="termsAndConditions"
Expand Down Expand Up @@ -360,6 +403,11 @@ export default function Download({
asset={asset}
/>
)}
{isContractingFeatureEnabled &&
asset?.metadata?.additionalInformation?.saas?.paymentMode ===
PAYMENT_MODES.PAYPERUSE &&
accountId &&
isAccountIdWhitelisted && <ContractingProvider did={asset.id} />}
{accountId && (
<WhitelistIndicator
accountId={accountId}
Expand Down
44 changes: 44 additions & 0 deletions src/components/Asset/AssetActions/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import axios from 'axios'
import { toast } from 'react-toastify'

export async function getContractingProviderNonce(
contractingBaseUrl: string,
address: string
) {
try {
const response = await axios.get(
`${contractingBaseUrl}/user/${address}/nonce`
)
return response.data
} catch (e) {
console.error(e)
toast.error('Could not get nonce from contracting provider.')
}
}

export async function getPayPerUseCount(
contractingBaseUrl: string,
address: string,
signature: string,
did: string
) {
try {
const response = await axios.post(
`${contractingBaseUrl}/contracting/validate`,
{ address, signature, did },
{
// throw only on status codes we dont expect
// 404 returned if no transaction was found for address & did combination
// 401 returned if 'hasAccess' for address on did is = false
validateStatus: (status) => status < 400 || [404, 401].includes(status)
}
)

const { data } = response

return data?.orderCount
} catch (e) {
console.error(e)
toast.error('Could not retrieve information from contracting provider.')
}
}
Loading