-
-
Notifications
You must be signed in to change notification settings - Fork 250
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
base: master
Are you sure you want to change the base?
Changes from 5 commits
d0a7d66
50addd2
aa4a534
aa904e0
2421da5
82c0420
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -25,7 +25,7 @@ export interface Subscription { | |
price: Amount | ||
possibleRenewals: string | ||
approvedRenewals: string | ||
type: 'Crypto' | 'Stripe' | ||
type: 'crypto' | 'stripe' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixing the types... based on openapi |
||
} | ||
|
||
interface GetSubscriptionsProps { | ||
|
@@ -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 || | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: { | ||
|
@@ -109,10 +130,7 @@ export const getSubscriptionsForLockByOwner = async ({ | |
keyId: tokenId, | ||
lockAddress, | ||
network, | ||
userAddress: key.owner, | ||
recurring: { | ||
[Op.gt]: 0, | ||
}, | ||
userAddress: ethereumAddress(key.owner), | ||
}, | ||
}) | ||
|
||
|
@@ -126,7 +144,7 @@ export const getSubscriptionsForLockByOwner = async ({ | |
...info, | ||
approvedRenewals, | ||
possibleRenewals: approvedRenewals, | ||
type: 'Stripe', | ||
type: 'stripe', | ||
}) | ||
} | ||
|
||
|
@@ -141,7 +159,7 @@ export const getSubscriptionsForLockByOwner = async ({ | |
...info, | ||
approvedRenewals: numberOfRenewalsApproved, | ||
possibleRenewals, | ||
type: 'Crypto', | ||
type: 'crypto', | ||
} | ||
|
||
subscriptions.push(cryptoSubscription) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 ?? {} | ||
|
@@ -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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for cards there is no refund. |
||
refetchInterval: false, | ||
meta: { | ||
errorMessage: | ||
|
@@ -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.') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
|
@@ -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 ? ( | ||
|
@@ -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 | ||
|
There was a problem hiding this comment.
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.