-
Notifications
You must be signed in to change notification settings - Fork 80
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
feat: reward bottom sheet #5024
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5024 +/- ##
==========================================
+ Coverage 85.46% 85.50% +0.03%
==========================================
Files 719 721 +2
Lines 29199 29368 +169
Branches 5038 5079 +41
==========================================
+ Hits 24955 25111 +156
- Misses 4014 4024 +10
- Partials 230 233 +3
Continue to review full report in Codecov by Sentry.
|
1 build increased size
Celo (test) 1.79.0 (144)
|
Item | Install Size Change |
---|---|
main.jsbundle | ⬆️ 12.3 kB |
🛸 Powered by Emerge Tools
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 haven't reviewed the tests because i had some questions about the implementation, but looks good!!
src/statsig/constants.ts
Outdated
@@ -111,6 +111,9 @@ export const DynamicConfigs = { | |||
configName: StatsigDynamicConfigs.NFT_CELEBRATION_CONFIG, | |||
defaultValues: { | |||
celebratedNft: {} as { networkId?: NetworkId; contractAddress?: string }, | |||
deepLink: '', | |||
expirationDate: new Date(0).toISOString(), | |||
reminderDate: new Date(0).toISOString(), |
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.
reminderDate: new Date(0).toISOString(), | |
rewardReminderDate: new Date(0).toISOString(), |
src/statsig/constants.ts
Outdated
@@ -111,6 +111,9 @@ export const DynamicConfigs = { | |||
configName: StatsigDynamicConfigs.NFT_CELEBRATION_CONFIG, | |||
defaultValues: { | |||
celebratedNft: {} as { networkId?: NetworkId; contractAddress?: string }, | |||
deepLink: '', | |||
expirationDate: new Date(0).toISOString(), |
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.
expirationDate: new Date(0).toISOString(), | |
rewardExpirationDate: new Date(0).toISOString(), |
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.
aside: any reason why you chose string instead of number for the dates? using number would remove the need to parse it to be a number in the saga later?
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.
because the strings are human readable. let machine do the conversion, not us 🤖
test/RootStateSchema.json
Outdated
@@ -2,6 +2,19 @@ | |||
"$schema": "http://json-schema.org/draft-07/schema#", | |||
"additionalProperties": false, | |||
"definitions": { | |||
"Action<any>": { |
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 strange / seemingly unrelated change?
|
||
const expired = isPast(expirationDate) | ||
if (expired) { | ||
return |
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 was wondering if we should thinking about the celebration separately to the reward - it kind of made sense in my head to celebrate the nft even if the user does not open the app in time to use their reward? i'll ask the question in the design channel, perhaps i've not got the correct mental model of this feature.
if we want to also hide the nft celebration if the expiry date is past though, i'd suggest resetting the state here rather than returning early. that way you can remove the date checks in the selectors?
) { | ||
return | ||
} | ||
|
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 having the early returns above, could we gate the below logic we want to execute with the NftCelebrationStatus
values that we care about? i found this logic a little hard to understand, but it seems we'd only want to execute logic if the NftCelebrationStatus === celebrationDisplayed, right?
src/home/celebration/NftReward.tsx
Outdated
|
||
const pillLabel = | ||
expirationStatus === ExpirationStatus.expired | ||
? t('nftCelebration.rewardBottomSheet.expired') |
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.
if the reward is expired, should we hide the sheet rather than show a user that they've got expired rewards?
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.
yes, hiding the sheet could be an option. with it, we have one string less to translate.
src/home/celebration/NftReward.tsx
Outdated
expired = 'expired', | ||
} | ||
|
||
// recalculates expiration status every second |
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.
if i understand correctly, this is necessary only towards the end of the reward period? i think that we can simplify this by setting an expiry date in statsig that is like 30min-1 hour before the actual expiry date - the worst case is that a user who is seeing this really close to the end of the program does not see the reward bottom sheet, however i think this tradeoff is worth the reduced code complexity? (we can also think of it like, we allow a grace period in the backend from the expiry time)
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.
yes, it's mostly for handling the situation when the reward expires while the bottom sheet is open.
I agree that we could expect that user wouldn't spend too much time looking at this bottom sheet and it's acceptable to not update expiration time and not auto dismiss the bottom sheet once it expires.
I like the idea of having a grace period, giving the late comers the chance to exercise their rewards.
src/home/celebration/NftReward.tsx
Outdated
? ExpirationStatus.aboutToExpire | ||
: ExpirationStatus.active, | ||
}) | ||
timeoutID.current = setTimeout(updateExpirationStatus, UPDATE_INTERVAL) |
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 recursion is kind of blowing my mind, why not do a setInterval instead?
src/home/celebration/NftReward.tsx
Outdated
type ExpirationPillProps = { status: ExpirationStatus; label: string } | ||
|
||
const ExpirationPill = ({ status, label }: ExpirationPillProps) => ( | ||
<View style={styles.pillContainer}> |
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: i don't think you need this div?
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.
LGTM!!
src/home/reducers.ts
Outdated
loading: boolean | ||
notifications: IdToNotification | ||
cleverTapInboxMessages: CleverTapInboxMessage[] | ||
hasVisitedHome: boolean | ||
nftCelebration: { | ||
networkId: NetworkId | ||
contractAddress: string | ||
displayed: boolean | ||
status: NftCelebrationStatus | ||
expirationDate: string |
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.
expirationDate: string | |
rewardExpirationDate: string |
src/home/actions.ts
Outdated
@@ -61,6 +85,9 @@ export type ActionTypes = | |||
| VisitHomeAction | |||
| CelebratedNftFoundAction | |||
| NftCelebrationDisplayedAction | |||
| NftRewardReadyToDisplayAction | |||
| NftRewardDisplayedAction | |||
| NftRewardReminderReadyToDisplayAction |
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: i wonder if NftRewardReadyToDisplayAction and NftRewardReminderReadyToDisplayAction could be combined, the action + reducer + their usage seem very similar, perhaps it'd be more simple to have a single action that takes a boolean showReminder
param?
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.
sure, thanks!
src/home/celebration/NftReward.tsx
Outdated
const { t } = useTranslation() | ||
const dispatch = useDispatch() | ||
|
||
const canShowNftReward = useSelector(showNftRewardSelector) |
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: maybe you don't need to check this again because it is checked in the WalletHome already?
src/home/celebration/NftReward.tsx
Outdated
const expirationDate = new Date(nftCelebration?.expirationDate ?? 0) | ||
const rewardReminderDate = new Date(nftCelebration?.rewardReminderDate ?? 0) | ||
|
||
const aboutToExpire = isToday(rewardReminderDate) || isPast(rewardReminderDate) |
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.
can we rely on the value of NftCelebrationStatus
here? the date is already used to determine the status in the findNftReward saga.
if not, can we remove the check for isPast
here? it is unlikely to be true but it feels weird to evaluate a past date as "about to expire". can we use isPast
to determine the value of isVisible
instead?
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 sorry, but I'm not getting the idea against isPast
.
we want to show this bottom sheet in a "reminder state" if the current date falls within the reminder period.
it is not necessary that the user will open the app on the first day of this period, that's why we want to check if the reminder date is in the past.
but I agree that instead of looking at the reminder date we can rely on the NftCelebrationStatus
instead. thanks for the suggestion!
src/home/celebration/NftReward.tsx
Outdated
} | ||
|
||
if (index === -1) { | ||
ValoraAnalytics.track(HomeEvents.nft_reward_dismiss, { |
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 think this is fine, but i think this dismiss event will always be fired (even if the accept event is also fired) - perhaps something we should note in the docs so that it's clear how to use this event. currently the doc says that this event is only triggered when the user dismisses the sheet (which i interpreted as dismiss without accepting the reward)
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.
that's a great catch! thanks!
the docs are correct: this event must dispatch only if the reward wasn't accepted. fixed it by adding a state variable.
sadly I can't figure out yet how to simulate bottom sheet dismiss event in tests to cover it with a test.
...typeScale.labelSemiBoldXSmall, | ||
paddingHorizontal: Spacing.Small12, | ||
paddingVertical: Spacing.Smallest8, | ||
borderRadius: 1000, |
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 big radius made me lol 😂
deepLink, | ||
} | ||
|
||
const showReminder = isToday(rewardReminderDate) || isPast(rewardReminderDate) |
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.
is isPast
supposed to be here?
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 believe, yes.
here we are checking if we are within the reminder period.
if user opens the app within the reminder period, but not exactly on the reminder start date, rewardReminderDate
would be in the past.
for example:
- user opened app: March, 2
rewardReminderDate
: March, 1 (isPast === true
)
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 of course, you're right. i was confusing this with the expiry date
return | ||
} | ||
|
||
if (!isSameNftContract(lastNftCelebration, celebratedNft)) { |
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 found this function still quite hard to understand because there are so many early return conditions. i think there's room to simplify this, e.g. the checks for whether network id / contract address are already done in isSameNftContract
, so we can remove some of the early return conditions relating to that? the bottom sheet itself also checks if the user owns the nft, so perhaps that condition can also be removed? it'd be ideal to have a condition that is the negative of all these early returns but idk if that's possible
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.
yes, thank for spotting this.
removed some redundant early returns.
expect(queryByText('nftCelebration.rewardBottomSheet.cta')).toBeNull() | ||
}) | ||
|
||
it('hanldes the cta correctly', () => { |
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.
it('hanldes the cta correctly', () => { | |
it('handles the cta correctly', () => { |
}) | ||
|
||
it('hanldes the cta correctly', () => { | ||
jest.useFakeTimers().setSystemTime(new Date('3000-11-01T00:00:00.000Z').getTime()) |
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 think we need to reset this mock in the beforeEach's probably? otherwise this will affect the next tests, not a big deal now but could potentially cost some head scratching time in the future (like #4953 (review))
### Description Show a bottom sheet with a description of a reward associated with a recently celebrated NFT (on NFT celebration see valora-inc#4886). The bottom sheet must show up: * only once within the app session * if the reward has not expired * after the NFT celebration was displayed or if the reward is about to expire https://github.com/valora-inc/wallet/assets/2737872/7ffb0a71-7f53-4e9f-be8e-1a9690bc61f0 The whole NFT celebration / reward / reminder journey is facilitated through reward status in a redux state: | Status | Expected behavior | |--------|--------| | `celebrationReadyToDisplay` | display NFT celebration | | `celebrationDisplayed` | check if we can display a reward | | `rewardReadyToDisplay` | display reward bottom sheet | | `rewardDisplayed` | check if we can display a reminder | | `reminderReadyToDisplay` | display reminder | | `reminderDisplayed` | end of journey | Notes: 1. The expiration pill copy when the reward is about to expire (brown one) deliberately doesn't follow the designs because of localization concerns. 2. The former `nftCelebration` state is discarded during migration, because it was never used yet. ### Test plan * Tested manually * Updated unit tests ### Related issues - Fixes RET-1002 ### Backwards compatibility Y ### Network scalability If a new NetworkId and/or Network are added in the future, the changes in this PR will: - [x] Continue to work without code changes, OR trigger a compilation error (guaranteeing we find it when a new network is added)
Description
Show a bottom sheet with a description of a reward associated with a recently celebrated NFT (on NFT celebration see #4886).
The bottom sheet must show up:
Simulator.Screen.Recording.-.iPhone.15.-.2024-03-04.at.12.38.48.mp4
The whole NFT celebration / reward / reminder journey is facilitated through reward status in a redux state:
celebrationReadyToDisplay
celebrationDisplayed
rewardReadyToDisplay
rewardDisplayed
reminderReadyToDisplay
reminderDisplayed
Notes:
nftCelebration
state is discarded during migration, because it was never used yet.Test plan
Related issues
Backwards compatibility
Y
Network scalability
If a new NetworkId and/or Network are added in the future, the changes in this PR will: