Skip to content

MBL-2998: Make some small improvements to RewardsCollectionViewModel#2775

Merged
amy-at-kickstarter merged 5 commits into
mainfrom
feat/adyer/mbl-2998/reward-carousel-cleanup
Mar 9, 2026
Merged

MBL-2998: Make some small improvements to RewardsCollectionViewModel#2775
amy-at-kickstarter merged 5 commits into
mainfrom
feat/adyer/mbl-2998/reward-carousel-cleanup

Conversation

@amy-at-kickstarter

@amy-at-kickstarter amy-at-kickstarter commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

📲 What

Make some improvements and refactors to RewardsCollectionViewModel, all as prep for shipping Featured Rewards.

  1. Put all of the code that filters rewards in one method, instead of spread throughout the file.
  2. Clean up the view model's init code, combining the signals filteredRewardsByLocation and rewards into one signal, rewards
  3. Fix a bug where the rewards context was set incorrectly

🤔 Why

As part of the change for featured rewards, RewardsCollectionViewModel will fetch a list of sorted rewards from the server. The list will be re-fetched when the shipping location changes. This refactoring makes it easier to plug in that API query.

self.shippingLocationSelectedSignal
)

let filteredByLocationRewards = Signal.combineLatest(rewards, self.shippingLocationSelectedSignal)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a little confusing, which is why I refactored it. There were two signals for rewards, which were switched on or combined. There was the rewards signal, which was sorted (but not filtered), and the filteredByLocationRewards signal, which was sorted and then filtered.

By doing some refactoring, I made this just one signal, rewards, which will be replaced with the new network fetch in a follow-up PR.

.filter { reward in isStartDateBeforeToday(for: reward) }
.map { reward in (project, reward, .pledge, nil) }
.map { project, rewards, location in
let context = userIsBackingProject(project) ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Looks like this has been wrong for a while, fixed it while I was in the neighborhood.

let isRewardLocalOrDigital = isRewardDigital(reward) || isRewardLocalPickup(reward)
let isUnrestrictedShippingReward = reward.isUnRestrictedShippingPreference
let isRestrictedShippingReward = reward.isRestrictedShippingPreference
private func shouldShowReward(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a very light refactoring of filteredRewardsByLocation. Should be a little clearer.

) -> Bool {
// Check if the reward isn't available yet.
// These are usually filtered out by the backend, but may be visible if you're the project creator.
if !isStartDateBeforeToday(for: reward) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the one filtering behavior tweak. I moved this out of self.reloadDataWithValues and into the filtering method itself, where it belongs.

n.B. we don't filter rewards that are no longer available, those are still displayed (with a disabled button).

let isRewardLocalOrDigital = isRewardDigital(reward) || isRewardLocalPickup(reward)
let isUnrestrictedShippingReward = reward.isUnRestrictedShippingPreference
let isRestrictedShippingReward = reward.isRestrictedShippingPreference
let isNoRewardReward = reward.isNoReward

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was checking if the reward was first in the list. Changed to noRewardReward for clarity, sicne that's what was intended.

guard let location else {
// This is a restricted reward, but the user hasn't selected a shipping location yet.
// Filter it out until a location is set.
return false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We don't actually display rewards until the shipping location is set, anyways.

}

func testRewardsOrdered() {
func testRewardsOrderedAndFiltered() {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Expanded this test a bit to handle some under-tested cases.

|> Reward.lens.shippingRulesExpanded .~ []
|> Reward.lens.shipping .~ Reward.Shipping(
enabled: true,
enabled: false,

@amy-at-kickstarter amy-at-kickstarter Mar 2, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These mock objects were wrong. When a reward is unshippable (local or digital), shippingEnabled is false. Here's an example GraphQL response showing some test rewards:

   [
          {
            "name": "pickup yer stickers",
            "shippingEnabled": false,
            "shippingPreference": "local",
            "localReceiptLocation": {
              "displayableName": "Brooklyn, NY"
            }
          },
          {
            "name": "Digital reward",
            "shippingEnabled": false,
            "shippingPreference": "none",
            "localReceiptLocation": null
          }
   ]

@amy-at-kickstarter amy-at-kickstarter marked this pull request as ready for review March 2, 2026 21:59
@amy-at-kickstarter amy-at-kickstarter requested review from a team and stevestreza-ksr and removed request for a team March 2, 2026 22:00

let rewards = project
.map(allowableSortedProjectRewards)
let shippingLocation = Signal.merge(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Just moved this up top.

@amy-at-kickstarter amy-at-kickstarter left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Some comments for whoever reviews this!

@stevestreza-ksr stevestreza-ksr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Appreciate the comments, this one was a little hard to follow along with.

Left one note about how this reward shipping restriction is handled but that's not caused by this PR, so seems fine to me

return false
}

assert(false, "A reward should either be restricted, or unrestricted. Showing in all locations.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this a restriction imposed on us from v1 API or something? It feels like we could catch this during validation of objects at parsing, since this would seem to suggest an invalid data state. Not a blocker, just noting this pattern smells a little.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hrm, yeah, very fair point about the smell. Looking into the helper functions like isRewardLocal, they all seem to be checking multiple fields:

!existingReward.shipping.enabled &&
    existingReward.localPickup != nil &&
    existingReward.shipping
    .preference.isAny(of: Reward.Shipping.Preference.local)

instead of just switching on the existing enum, the Reward.Shipping.Preference.

It's not clear to me if you can end up in a state where, say, your shipping preference is local but the localPickup object isn't set. So I agree with you, I like the idea of dealing with that in parsing, not here. (And probably this should just be an enum? You can't be a locally-picked up digital object, now can you?)

I filed a follow-up ticket MBL-3104 to look into this.

@amy-at-kickstarter amy-at-kickstarter changed the title MBL-2988: Make some small improvements to RewardsCollectionViewModel MBL-2998: Make some small improvements to RewardsCollectionViewModel Mar 5, 2026
@amy-at-kickstarter amy-at-kickstarter merged commit aafdf96 into main Mar 9, 2026
7 checks passed
@amy-at-kickstarter amy-at-kickstarter deleted the feat/adyer/mbl-2998/reward-carousel-cleanup branch March 9, 2026 14:23
amy-at-kickstarter added a commit that referenced this pull request Mar 12, 2026
…ewModel (#2775)"

This reverts commit aafdf96.

This caused a bug where the estimated shipping location was missing. Reverting this on
the release branch.
amy-at-kickstarter added a commit that referenced this pull request Mar 12, 2026
…ewModel (#2775)" (#2794)

This reverts commit aafdf96.

This caused a bug where the estimated shipping location was missing. Reverting this on
the release branch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants