-
-
Notifications
You must be signed in to change notification settings - Fork 114
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
Replace toasts with improved custom optimistic UX #1161
Conversation
WalkthroughThe modifications involve a series of enhancements and refactorings across various components, emphasizing payment handling, error management, and user experience improvements in response handling. Key changes include the introduction of new payment and error handling utilities, modifications to form and button components to integrate these utilities, and updates to the UI components to reflect pending states and improve interaction feedback. Changes
Assessment against linked issues
Possibly related issues
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (invoked as PR comments)
Additionally, you can add CodeRabbit Configration File (
|
2de9cd1
to
4b083ab
Compare
Random better idea for zap undos: instead of this ugly orange toast with "undo" written inside, use the pending pulse animation I had before and use that as undo. So while the bolt is pulsing, you can click it again to undo. But if you're too slow, you'll zap your default, lol. I think @notnout mentioned this idea for undos first. update: Implemented in 48c0142 |
8ca84df
to
f829627
Compare
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.
Actionable comments posted: 9
Out of diff range and nitpick comments (6)
components/webln/nwc.js (2)
210-214
: Optimize the timeout handling in thesendPayment
function to align with best practices.Consider using a named function for the timeout callback to improve readability and potential reusability.
210-214
: Enhance the robustness of the payment process by adding more detailed logging and error information.Consider adding more detailed logs and error information to help with debugging and maintaining the payment process.
components/reply.js (1)
270-280
: Enhance the robustness of the reply process by adding more detailed logging and error information.Consider adding more detailed logs and error information to help with debugging and maintaining the reply process, especially in error scenarios.
components/item-act.js (1)
104-116
: Enhance the robustness of the action process by adding more detailed logging and error information.Consider adding more detailed logs and error information to help with debugging and maintaining the action process, especially in error scenarios.
pages/settings/index.js (1)
29-29
: Consider adding a comment explaining the purpose ofINVOICE_RETENTION_DAYS
andZAP_UNDO_DELAY
.Adding comments for these constants can improve code readability and maintainability, especially for new developers or when revisiting this section after a long time.
components/form.js (1)
Line range hint
805-868
: Refactor theForm
component to integrate new hooks and error handling.- const payment = usePayment() - const me = useMe() + const { request } = usePayment() + const { user } = useMe()Ensure destructuring is used for consistency and to avoid potential bugs related to direct property access on hook return values.
const onSubmitInner = useCallback(async ({ amount, ...values }, ...args) => { | ||
let cancel, revert, beforeSubmitReturn | ||
try { | ||
if (onSubmit) { | ||
// extract cost from formik fields | ||
// (cost may also be set in a formik field named 'amount') | ||
const cost = feeButton?.total || values?.amount | ||
if (cost) { | ||
values.cost = cost | ||
if (requireSession && !me) { | ||
throw new SessionRequiredError() | ||
} | ||
beforeSubmitReturn = beforeSubmit?.({ amount, ...values }) | ||
let hash, hmac | ||
if (invoiceable) { | ||
revert = await optimisticUpdate?.({ amount, ...values }, ...args); | ||
[{ hash, hmac }, cancel] = await payment.request(amount) | ||
} | ||
await onSubmit(values, ...args) | ||
await onSubmit({ hash, hmac, amount, ...values }, ...args) | ||
if (!storageKeyPrefix) return | ||
clearLocalStorage(values) | ||
} | ||
} catch (err) { | ||
revert?.() | ||
if (err instanceof InvoiceCanceledError || err instanceof ActCanceledError) { | ||
return | ||
} | ||
const msg = err.message || err.toString?.() | ||
// ignore errors from JIT invoices or payments from attached wallets | ||
// that mean that submit failed because user aborted the payment | ||
if (msg === 'modal closed' || msg === 'invoice canceled') return | ||
toaster.danger('submit error: ' + msg) | ||
if (onError) { | ||
onError({ amount, ...values, reason: msg }) | ||
} else { | ||
toaster.danger('submit error: ' + msg) | ||
} | ||
cancel?.() | ||
} finally { | ||
afterSubmit?.({ amount, ...values, ...beforeSubmitReturn }) | ||
} | ||
}, [onSubmit, feeButton?.total, toaster, clearLocalStorage, storageKeyPrefix]) | ||
}, [me, onSubmit, feeButton?.total, toaster, clearLocalStorage, storageKeyPrefix, payment]) |
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.
Enhance the onSubmitInner
function with session checks and payment handling.
- if (requireSession && !me) {
+ if (requireSession && !user) {
throw new SessionRequiredError()
}
- [{ hash, hmac }, cancel] = await payment.request(amount)
+ const { hash, hmac, cancel } = await request(amount)
Refactor to use destructuring for clarity and to handle the case where payment.request
might return an object instead of an array.
Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.
const onSubmitInner = useCallback(async ({ amount, ...values }, ...args) => { | |
let cancel, revert, beforeSubmitReturn | |
try { | |
if (onSubmit) { | |
// extract cost from formik fields | |
// (cost may also be set in a formik field named 'amount') | |
const cost = feeButton?.total || values?.amount | |
if (cost) { | |
values.cost = cost | |
if (requireSession && !me) { | |
throw new SessionRequiredError() | |
} | |
beforeSubmitReturn = beforeSubmit?.({ amount, ...values }) | |
let hash, hmac | |
if (invoiceable) { | |
revert = await optimisticUpdate?.({ amount, ...values }, ...args); | |
[{ hash, hmac }, cancel] = await payment.request(amount) | |
} | |
await onSubmit(values, ...args) | |
await onSubmit({ hash, hmac, amount, ...values }, ...args) | |
if (!storageKeyPrefix) return | |
clearLocalStorage(values) | |
} | |
} catch (err) { | |
revert?.() | |
if (err instanceof InvoiceCanceledError || err instanceof ActCanceledError) { | |
return | |
} | |
const msg = err.message || err.toString?.() | |
// ignore errors from JIT invoices or payments from attached wallets | |
// that mean that submit failed because user aborted the payment | |
if (msg === 'modal closed' || msg === 'invoice canceled') return | |
toaster.danger('submit error: ' + msg) | |
if (onError) { | |
onError({ amount, ...values, reason: msg }) | |
} else { | |
toaster.danger('submit error: ' + msg) | |
} | |
cancel?.() | |
} finally { | |
afterSubmit?.({ amount, ...values, ...beforeSubmitReturn }) | |
} | |
}, [onSubmit, feeButton?.total, toaster, clearLocalStorage, storageKeyPrefix]) | |
}, [me, onSubmit, feeButton?.total, toaster, clearLocalStorage, storageKeyPrefix, payment]) | |
const onSubmitInner = useCallback(async ({ amount, ...values }, ...args) => { | |
let cancel, revert, beforeSubmitReturn | |
try { | |
if (onSubmit) { | |
if (requireSession && !user) { | |
throw new SessionRequiredError() | |
} | |
beforeSubmitReturn = beforeSubmit?.({ amount, ...values }) | |
let hash, hmac | |
if (invoiceable) { | |
revert = await optimisticUpdate?.({ amount, ...values }, ...args); | |
const { hash, hmac, cancel } = await request(amount) | |
} | |
await onSubmit({ hash, hmac, amount, ...values }, ...args) | |
if (!storageKeyPrefix) return | |
clearLocalStorage(values) | |
} | |
} catch (err) { | |
revert?.() | |
if (err instanceof InvoiceCanceledError || err instanceof ActCanceledError) { | |
return | |
} | |
const msg = err.message || err.toString?.() | |
if (onError) { | |
onError({ amount, ...values, reason: msg }) | |
} else { | |
toaster.danger('submit error: ' + msg) | |
} | |
cancel?.() | |
} finally { | |
afterSubmit?.({ amount, ...values, ...beforeSubmitReturn }) | |
} | |
}, [me, onSubmit, feeButton?.total, toaster, clearLocalStorage, storageKeyPrefix, payment]) |
const timeout = JIT_INVOICE_TIMEOUT_MS | ||
const timer = setTimeout(() => { | ||
const msg = 'timeout waiting for info event' | ||
const msg = 'timeout waiting for payment' | ||
logger.error(msg) | ||
reject(new Error(msg)) | ||
reject(new InvoiceExpiredError()) |
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.
Refactor the error handling in the sendPayment
function to ensure consistency and clarity.
- const timeout = JIT_INVOICE_TIMEOUT_MS
- const timer = setTimeout(() => {
- const msg = 'timeout waiting for payment'
- logger.error(msg)
- reject(new InvoiceExpiredError())
+ const timer = setTimeout(() => {
+ logger.error('timeout waiting for payment')
+ reject(new InvoiceExpiredError('Payment timeout reached'))
Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.
const timeout = JIT_INVOICE_TIMEOUT_MS | |
const timer = setTimeout(() => { | |
const msg = 'timeout waiting for info event' | |
const msg = 'timeout waiting for payment' | |
logger.error(msg) | |
reject(new Error(msg)) | |
reject(new InvoiceExpiredError()) | |
const timer = setTimeout(() => { | |
logger.error('timeout waiting for payment') | |
reject(new InvoiceExpiredError('Payment timeout reached')) |
const cacheAddComment = (cache, parentId, data) => { | ||
cache.modify({ | ||
id: `Item:${parentId}`, | ||
fields: { | ||
comments (existingCommentRefs = []) { | ||
const newCommentRef = cache.writeFragment({ | ||
data, | ||
fragment: COMMENTS, | ||
fragmentName: 'CommentsRecursive' | ||
}) | ||
return [newCommentRef, ...existingCommentRefs] | ||
} | ||
} | ||
}) | ||
} |
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.
Refactor the cacheAddComment
function to improve readability and maintainability.
- cache.modify({
- id: `Item:${parentId}`,
- fields: {
- comments (existingCommentRefs = []) {
- const newCommentRef = cache.writeFragment({
- data,
- fragment: COMMENTS,
- fragmentName: 'CommentsRecursive'
- })
- return [newCommentRef, ...existingCommentRefs]
- }
- }
- })
+ const newCommentRef = cache.writeFragment({
+ data,
+ fragment: COMMENTS,
+ fragmentName: 'CommentsRecursive'
+ })
+ cache.modify({
+ id: `Item:${parentId}`,
+ fields: {
+ comments (existingCommentRefs = []) {
+ return [newCommentRef, ...existingCommentRefs]
+ }
+ }
+ })
Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.
const cacheAddComment = (cache, parentId, data) => { | |
cache.modify({ | |
id: `Item:${parentId}`, | |
fields: { | |
comments (existingCommentRefs = []) { | |
const newCommentRef = cache.writeFragment({ | |
data, | |
fragment: COMMENTS, | |
fragmentName: 'CommentsRecursive' | |
}) | |
return [newCommentRef, ...existingCommentRefs] | |
} | |
} | |
}) | |
} | |
const cacheAddComment = (cache, parentId, data) => { | |
const newCommentRef = cache.writeFragment({ | |
data, | |
fragment: COMMENTS, | |
fragmentName: 'CommentsRecursive' | |
}) | |
cache.modify({ | |
id: `Item:${parentId}`, | |
fields: { | |
comments (existingCommentRefs = []) { | |
return [newCommentRef, ...existingCommentRefs] | |
} | |
} | |
}) | |
} |
// prevent edit timer jump | ||
const pendingComment = cacheRemovePendingComment(cache, parentId) | ||
const comment = { ...upsertComment, createdAt: pendingComment.createdAt } | ||
cacheAddComment(cache, parentId, comment) | ||
|
||
const ancestors = item.path.split('.') | ||
|
||
// update all ancestors | ||
ancestors.forEach(id => { | ||
cache.modify({ | ||
id: `Item:${id}`, | ||
fields: { | ||
ncomments (existingNComments = 0) { | ||
return existingNComments + 1 | ||
} | ||
} | ||
}) | ||
}) | ||
// XXX cache update already applied | ||
// cacheUpdateAncestors(cache, ancestors) |
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.
Optimize the reply submission and cache update logic to enhance performance and clarity.
Consider restructuring the logic to separate concerns more clearly, potentially splitting the function into smaller, more focused functions.
const optimisticUpdate = useCallback(({ text }, { resetForm }) => { | ||
setReply(replyOpen || false) | ||
resetForm({ text: '' }) | ||
return upsertCommentOptimisticUpdate(cache, { ...item, text }, { me }) | ||
}, [setReply, cache, item, me]) |
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.
Refine the optimistic update logic to ensure it aligns with the overall application architecture and error handling strategy.
Consider reviewing and potentially refactoring the optimistic update logic to ensure it integrates seamlessly with the rest of the application, particularly in terms of error handling and state management.
const updateItemSats = (cache, { id, path, act, sats }, { me }) => { | ||
if (sats < 0) { | ||
// if sats < 0, we are reverting. | ||
// in that case, it is important that we first revert the persisted sats | ||
// before calling cache.modify since cache.modify will trigger field reads | ||
// and thus persisted sats will be counted | ||
if (!me) persistItemAnonSats({ id, path, act, sats }) | ||
else persistItemPendingSats({ id, path, act, sats }) | ||
} | ||
|
||
const update = useCallback((cache, args) => { | ||
const { data: { act: { id, sats, path, act } } } = args | ||
cache.modify({ | ||
id: `Item:${id}`, | ||
fields: { | ||
sats (existingSats = 0) { | ||
return act === 'TIP' ? existingSats + sats : existingSats | ||
}, | ||
meSats (existingSats = 0) { | ||
return act === 'TIP' ? existingSats + sats : existingSats | ||
}, | ||
meDontLikeSats: me | ||
? (existingSats = 0) => { | ||
return act === 'DONT_LIKE_THIS' ? existingSats + sats : existingSats | ||
} | ||
: undefined | ||
} | ||
}) | ||
|
||
cache.modify({ | ||
id: `Item:${id}`, | ||
fields: { | ||
sats (existingSats = 0) { | ||
if (act === 'TIP') { | ||
return existingSats + sats | ||
if (act === 'TIP') { | ||
// update all ancestors | ||
path.split('.').forEach(aId => { | ||
if (Number(aId) === Number(id)) return | ||
cache.modify({ | ||
id: `Item:${aId}`, | ||
fields: { | ||
commentSats (existingCommentSats = 0) { | ||
return existingCommentSats + sats | ||
} | ||
} | ||
}) | ||
}) | ||
} | ||
|
||
return existingSats | ||
}, | ||
meSats: me | ||
? (existingSats = 0) => { | ||
if (act === 'TIP') { | ||
return existingSats + sats | ||
} | ||
if (sats > 0) { | ||
if (!me) persistItemAnonSats({ id, path, act, sats }) | ||
else persistItemPendingSats({ id, path, act, sats }) | ||
} | ||
} |
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.
Optimize the action submission and cache update logic to enhance performance and clarity.
Consider restructuring the logic to separate concerns more clearly, potentially splitting the function into smaller, more focused functions.
const optimisticUpdate = useCallback(async ({ amount }) => { | ||
onClose() | ||
strike() | ||
const revert = actOptimisticUpdate(cache, { ...item, sats: Number(amount), act: down ? 'DONT_LIKE_THIS' : 'TIP' }, { me }) | ||
abortSignal?.start() | ||
if (zapUndoTrigger(me, amount)) { | ||
try { | ||
await zapUndo(abortSignal) | ||
} catch (err) { | ||
revert() | ||
throw err | ||
} | ||
} else { | ||
abortSignal?.done() | ||
} | ||
) | ||
return revert | ||
}, [cache, strike, onClose, abortSignal]) |
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.
Refine the optimistic update logic to ensure it aligns with the overall application architecture and error handling strategy.
Consider reviewing and potentially refactoring the optimistic update logic to ensure it integrates seamlessly with the rest of the application, particularly in terms of error handling and state management.
revert?.() | ||
if (error instanceof InvoiceCanceledError || error instanceof ActCanceledError) { | ||
return | ||
} | ||
console.error(error) | ||
toaster.danger('zap: ' + error?.message || error?.toString?.()) | ||
const reason = error?.message || error?.toString?.() | ||
if (me) notify(NotificationType.ZapError, { reason, amount: sats - meSats, itemId: item.id }) | ||
else toaster.danger('zap error: ' + reason) | ||
cancel?.() | ||
} finally { |
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.
Enhance the error handling in the action functionality to ensure robustness and clarity.
Consider refining the error handling logic to ensure it is consistent and clear, potentially adding more detailed error messages and handling specific error types more effectively.
const filtered = JSON.parse(stored).filter(({ sortTime }) => { | ||
// only keep notifications younger than 24 hours | ||
return new Date(sortTime) >= datePivot(new Date(), { hours: -24 }) | ||
}) |
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.
Only keep notifications younger than 24 hours
This helps with not bloating /notifications and local storage.
It also is a workaround for having to come up with some complicated pagination algorithm which considers how many / which client notifications should be included on each page.
-- 5ff6210
I think this is smart lazy stuff
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.
This is a reasonable shim for sure
components/item-info.js
Outdated
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.
I removed the local field item.meAnonSats
because I realized I can simply add them to item.meSats
via the read
function
@@ -1,5 +1,6 @@ | |||
.toastContainer { | |||
transform: translate3d(0, 0, 0); | |||
margin-bottom: 53px; /* padding for bottom nav bar on mobile */ |
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.
unrelated fix for toasts shadowing bottom nav bar
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.
This looks really thoughtfully done on first pass. I'll do a deep review and QA the first chance I get tomorrow.
components/dont-link-this.js
Outdated
onClose={(amount) => { | ||
onClose() |
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.
nit but onClose={onClose}
?
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.
Huch, didn't notice this on my crusade against old zap undo code. Good catch
components/fee-button.js
Outdated
} | ||
}, [baseLineItems, lineItems, remoteLineItems, mergeLineItems, disabled, setDisabled]) | ||
}, [me, baseLineItems, lineItems, remoteLineItems, mergeLineItems, disabled, setDisabled]) |
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.
nit but this will rerender every second because me
updates on poll
}, [me, baseLineItems, lineItems, remoteLineItems, mergeLineItems, disabled, setDisabled]) | |
}, [me?.privates?.sats, baseLineItems, lineItems, remoteLineItems, mergeLineItems, disabled, setDisabled]) |
export class SessionRequiredError extends Error { | ||
constructor () { | ||
super('session required') | ||
this.name = 'SessionRequiredError' | ||
} | ||
} |
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.
So that's how you create a custom javascript error! 😆
} | ||
await onSubmit(values, ...args) | ||
await onSubmit({ hash, hmac, amount, ...values }, ...args) |
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.
This changes the signature of onSubmit
. Does anything use args?
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.
I'm only adding hash, hmac, amount to values which was already an object. I don't think this changes the signature.
You might missed the curly braces around the comma separated values?
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.
Oh right, values was already an object. What's in args?
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.
formik helper stuff like resetForm
. They call it "formikBag" in the documentation. Maybe we should use that name, too. Since we no longer wrap submit handlers for our payment flow, I think args
is really the same as formikBag
.
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.
Oh I see. I remember using resetForm
at one point.
Someone at Square told me they don't allow engineers to declare array indices as i
. Kind of intense lol
Somehow I end up increasing my balance with this approach. Aren't those invoices not supposed to settle? Also, one of my zaps ended up showing up as a 0-sat flag (on the sn wallet plans post). No idea what that's about, but I suspect there's a bug somewhere that caused that. Did you test all sending side wallets after these changes? It might help discover more unhappy paths. Screen.Recording.2024-05-13.at.10.52.30.AM.mp4After I stopped recording this video my balance went up to 200 sats. |
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.
Failing QA
You're right, they should either settle because they were consumed by an action or canceled. So in both cases, the balance shouldn't increase.
I had this error before. It happens when a payment fails and the code that reverts the optimistic update ends up writing a negative value instead of the value before the update was done. I thought I fixed it 🤔
No, I assumed that all sending wallets would behave the same since I only relied on |
Kind of meta, but because all the wallets are all in a This reminds me, I need to get NWC working locally and add a LNBits container. |
#1151 should have you covered. You only need to install
Yep, also thought about this. I'll do this today. I think this is more in my ball park anyway since I also use LNbits to manage my lightning credentials for hnbot and oracle + I need this to test this PR. |
81d7805
to
3ddd6b4
Compare
Mhh, I fixed a race condition in 3ddd6b4 but there is still one: when we zap too fast, pending sats are counted again for each zap since For some reason I completely missed this before, I must have been too cautious in testing my code or NWC payments were too slow so I didn't notice this. I tried to fix this by accounting for existing pending sats when calling This also isn't the only issue I found: We don't update I'm starting to believe we should straight go for storing pending states in the server and skip this release of client-side optimistic updates. On the server, we can use database transactions to make sure we don't run into race conditions like this. Feels bad that it took me so long to realize that doing this on the client is way too messy and buggy but better pivoting late than releasing this in its current state. Going to sleep over this but I think this is the right call. Doing all of this stuff on the server should be much simpler, thus cleaner and is what we wanted to do anyway. |
This also means we can use item links as for other notifications since they weren't added to client notifications to make the dismissal button work properly.
This helps with not bloating /notifications and local storage. It also is a workaround for having to come up with some complicated pagination algorithm which considers how many / which client notifications should be included on each page.
By populating n.item during render, we no longer use stale item data that was set during notify(). This also fixes following issues: * the need to add item.root manually to n.item * the need to populate the cache such that readFragment during optimistic updates finds item fragments
If zapping too fast, item.meSats inside optimistic update could include pending sats from previous zap unlike item.meSats in calling context. This is fixed by passing satsDelta instead of calculating it independently with readFragment. This race condition lead to following unexpected behavior that is now fixed: * pending sats could go negative causing items to falsely be marked as flagged unrelated fix: * fix zapUndoTrigger using total sats not satsDelta
Superseded by #1184 |
Description
Close #849 Related to #848
As mentioned in #1071, this replaces
useInvoiceable
withusePayment
which has a simpler API.This PR also contains changes to improve the external payment UX by replacing generic toasts with custom UX code per action.
TODO:
optimistic post UXcanceled because this is most likely better implemented using server-side code so let's first get user feedback on client-side optimistic update code before delving deeper into UX patterns that include server-side codebounty formdiscussion formlink formpoll formjob formterritory admin optimistic UXcreateupdateunarchivemeAnonSats
not immediately updatedError handling
remove fallback to QR code on attached wallet payment failure since it interrupts UX (rely on out-of-band error handling like notifications instead) (?)let's see in production firstCannot read properties of null (reading 'meSats')
when reloading page on /notifications and trying to zap error notification because cache is not populatedScreenshots
Since toasts for success and error were removed for better UX, we now use
dismissibleerror notifications that are only stored locally. So if you zap something and it fails, it looks like this:Additional Context
See #1071 (comment) for additional context regarding move from
useInvoiceable
tousePayment
. Most notably,JITInvoice
is no longer with us. I removed it because it made the code more complicated and the retry/cancel UX was bad and lead regularly to confusion ("sats received" vs "error"). The HODL invoice is now immediately canceled if the action fails. With the new custom optimistic UX, retries via literally retrying (pressingreply
, zapping again etc.) are imo simple enough.Checklist
Are your changes backwards compatible? Please answer below:
Did you QA this? Could we deploy this straight to production? Please answer below:
For frontend changes: Tested on mobile? Please answer below:
Did you introduce any new environment variables? If so, call them out explicitly here: