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

feature(unlock-app, locksmith): Cancel card paid memberships #13123

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
22 changes: 14 additions & 8 deletions locksmith/src/controllers/v2/subscriptionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import subscriptionOperations, {

export class SubscriptionController {
/**
* Get an active crypto or fiat subscription associated with the key. This will return next renewal date, possible number of renewals, approved number of renewals, and other details.
* Get an active crypto or fiat subscription associated with the key.
* This will return next renewal date, possible number of renewals, approved number of renewals, and other details.
*/
async getSubscription(request: Request, response: Response) {
const network = Number(request.params.network)
Expand All @@ -33,14 +34,19 @@ export class SubscriptionController {
const lockAddress = normalizer.ethereumAddress(request.params.lockAddress)
const keyId = Number(request.params.keyId)
const userAddress = normalizer.ethereumAddress(request.user!.walletAddress)
await KeySubscription.destroy({
where: {
keyId,
lockAddress,
network,
userAddress,
await KeySubscription.update(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of deleting we just move to 0 recurring.

{
recurring: 0,
},
})
{
where: {
keyId,
lockAddress,
network,
userAddress,
},
}
)
return response.sendStatus(204)
}
}
76 changes: 47 additions & 29 deletions locksmith/src/operations/subscriptionOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
} from '@unlock-protocol/unlock-js'
import { ethers } from 'ethers'
import { KeySubscription } from '../models'
import { Op } from 'sequelize'

import dayjs from '../config/dayjs'
import { ethereumAddress } from '../utils/normalizer'

interface Amount {
amount: string
Expand All @@ -25,7 +25,7 @@ export interface Subscription {
price: Amount
possibleRenewals: string
approvedRenewals: string
type: 'Crypto' | 'Stripe'
type: 'crypto' | 'stripe'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixing the types... based on openapi

}

interface GetSubscriptionsProps {
Expand Down Expand Up @@ -54,42 +54,63 @@ export const getSubscriptionsForLockByOwner = async ({
)

// If no key is found or not erc20 or version < 11 which we don't fully support, return nothing.
if (
!key ||
key.lock.tokenAddress === ethers.constants.AddressZero ||
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may actually have card recurring even if the lock itself does not support recurring thru an ERC20.

parseInt(key.lock.version) < 11
) {
if (!key || parseInt(key.lock.version) < 11) {
return []
}

const web3Service = new Web3Service(networks)
const provider = web3Service.providerForNetwork(network)
const [userBalance, decimals, userAllowance, symbol] = await Promise.all([
getErc20BalanceForAddress(key.lock.tokenAddress, key.owner, provider),
getErc20Decimals(key.lock.tokenAddress, provider),
getAllowance(key.lock.tokenAddress, key.lock.address, provider, key.owner),
getErc20TokenSymbol(key.lock.tokenAddress, provider),
])

const balance = ethers.utils.formatUnits(userBalance, decimals)

const price = key.lock.price

let userBalance,
decimals,
userAllowance,
symbol,
numberOfRenewalsApprovedValue,
numberOfRenewalsApproved

if (
key.lock.tokenAddress &&
key.lock.tokenAddress !== ethers.constants.AddressZero
) {
;[userBalance, decimals, userAllowance, symbol] = await Promise.all([
getErc20BalanceForAddress(key.lock.tokenAddress, key.owner, provider),
getErc20Decimals(key.lock.tokenAddress, provider),
getAllowance(
key.lock.tokenAddress,
key.lock.address,
provider,
key.owner
),
getErc20TokenSymbol(key.lock.tokenAddress, provider),
])

// Approved renewals
numberOfRenewalsApprovedValue =
userAllowance.gt(0) && parseFloat(price) > 0
? userAllowance.div(price)
: ethers.BigNumber.from(0)
Comment on lines +66 to +93
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is hard to read. Can we split it down?


numberOfRenewalsApproved = numberOfRenewalsApprovedValue.toString()
} else {
userBalance = await provider.getBalance(key.owner)
decimals = networks[network].nativeCurrency.decimals
userAllowance = 0
symbol = networks[network].nativeCurrency.symbol
numberOfRenewalsApprovedValue = '0'
numberOfRenewalsApproved = '0'
}

const balance = ethers.utils.formatUnits(userBalance, decimals)

const next =
key.expiration === ethers.constants.MaxUint256.toString()
? null
: dayjs.unix(key.expiration).isBefore(dayjs())
? null
: parseInt(key.expiration)

// Approved renewals
const numberOfRenewalsApprovedValue =
userAllowance.gt(0) && parseFloat(price) > 0
? userAllowance.div(price)
: ethers.BigNumber.from(0)

const numberOfRenewalsApproved = numberOfRenewalsApprovedValue.toString()

const info = {
next,
balance: {
Expand All @@ -109,10 +130,7 @@ export const getSubscriptionsForLockByOwner = async ({
keyId: tokenId,
lockAddress,
network,
userAddress: key.owner,
recurring: {
[Op.gt]: 0,
},
userAddress: ethereumAddress(key.owner),
},
})

Expand All @@ -126,7 +144,7 @@ export const getSubscriptionsForLockByOwner = async ({
...info,
approvedRenewals,
possibleRenewals: approvedRenewals,
type: 'Stripe',
type: 'stripe',
})
}

Expand All @@ -141,7 +159,7 @@ export const getSubscriptionsForLockByOwner = async ({
...info,
approvedRenewals: numberOfRenewalsApproved,
possibleRenewals,
type: 'Crypto',
type: 'crypto',
}

subscriptions.push(cryptoSubscription)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ import { useMutation, useQuery } from '@tanstack/react-query'
import { ToastHelper } from '../../helpers/toast.helper'
import { useKeychain } from '~/hooks/useKeychain'
import { useAuth } from '~/contexts/AuthenticationContext'
import { storage } from '~/config/storage'

export interface CancelAndRefundProps {
isOpen: boolean
lock: any
setIsOpen: (open: boolean) => void
account: string
currency: string
tokenId: string
network: number
onExpireAndRefund?: () => void
subscription: any
}

const MAX_TRANSFER_FEE = 10000
Expand All @@ -23,10 +24,10 @@ export const CancelAndRefundModal = ({
lock,
setIsOpen,
account: owner,
currency,
tokenId,
network,
onExpireAndRefund,
subscription,
}: CancelAndRefundProps) => {
const { getWalletService } = useAuth()
const { address: lockAddress, tokenAddress } = lock ?? {}
Expand All @@ -43,7 +44,7 @@ export const CancelAndRefundModal = ({
['getAmounts', lockAddress],
getAmounts,
{
enabled: isOpen, // execute query only when the modal is open
enabled: isOpen && subscription.type !== 'Stripe',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for cards there is no refund.

refetchInterval: false,
meta: {
errorMessage:
Expand All @@ -59,18 +60,22 @@ export const CancelAndRefundModal = ({
lockAddress,
tokenId,
}
const walletService = await getWalletService(network)
if (subscription.type === 'stripe') {
await storage.cancelSubscription(network, lockAddress, tokenId)
} else {
const walletService = await getWalletService(network)

return walletService.cancelAndRefund(
params,
{} /** transactionParams */,
() => true
)
return walletService.cancelAndRefund(
params,
{} /** transactionParams */,
() => true
)
}
}

const cancelRefundMutation = useMutation(cancelAndRefund, {
onSuccess: () => {
ToastHelper.success('Key cancelled and successfully refunded.')
ToastHelper.success('Key cancelled.')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the mention of refund as it might not be applicable!

setIsOpen(false)
if (typeof onExpireAndRefund === 'function') {
onExpireAndRefund()
Expand All @@ -86,15 +91,13 @@ export const CancelAndRefundModal = ({
},
})

const hasMaxCancellationFee = Number(transferFee) >= MAX_TRANSFER_FEE
const isRefundable =
!hasMaxCancellationFee && refundAmount <= Number(lockBalance)
const hasRefund =
Number(transferFee) < MAX_TRANSFER_FEE || subscription.type !== 'Stripe'
const isRefundable = refundAmount <= Number(lockBalance)

const buttonDisabled =
isLoading || !isRefundable || cancelRefundMutation?.isLoading

if (!lock) return <span>No lock selected</span>

return (
<Modal isOpen={isOpen} setIsOpen={setIsOpen}>
{isLoading ? (
Expand All @@ -112,21 +115,23 @@ export const CancelAndRefundModal = ({
Cancel and Refund
</h3>
<p className="mt-2 text-md">
{hasMaxCancellationFee ? (
{hasRefund ? (
<span>This key is not refundable.</span>
) : isRefundable ? (
<>
<span>
{currency} {parseFloat(`${refundAmount}`!).toFixed(3)}
{lock.currencySymbol}{' '}
{parseFloat(`${refundAmount}`!).toFixed(3)}
</span>
{` will be refunded, Do you want to proceed?`}
{` will be refunded.`}
</>
) : (
<span>
Refund is not possible because the contract does not have
funds to cover it.
</span>
)}
)}{' '}
Do you want to proceed?
</p>
</div>
<Button
Expand Down
Loading
Loading