Skip to content

feat: deploy seen-once logic for goal in review modal in my kiva and … - #7148

Open
christian14b wants to merge 6 commits into
mainfrom
MP-3063
Open

feat: deploy seen-once logic for goal in review modal in my kiva and …#7148
christian14b wants to merge 6 commits into
mainfrom
MP-3063

Conversation

@christian14b

@christian14b christian14b commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

…portfolio pages

Opens the Goal in Review modal, once per user in total across both My Kiva and Portfolio pages.

What changed

  1. A seen-once flag the feature owns. goalRecapViewed[year] is written to user preferences when the recap opens, so opening and dismissing both count as seen. It is server side, so it holds across pages, sessions and devices (a cookie would not).

  2. A pending marker for the "session after completion" rule. goalRecapPending[year] is set the first time a completed goal is seen, and the recap opens on the visit after that.

  3. ** Both pages ask the same question.** Every rule about when the recap should appear lives in one place, shouldAutoOpenRecap. It takes the things it needs as arguments and answers yes or no, so each rule can be tested on its own without standing up a page.

  4. Portfolio now renders the modal. It computed goalInReviewEnable but never had GoalInReviewModal mounted.

Trigger rules

Goal state Behaviour
Completed, first sighting nothing shown, arms goalRecapPending[year]
Completed, already pending opens, marks goalRecapViewed[year]
In progress opens on the first visit once the flag is on
Already seen never again, on either page
Previous year or expired never
No goal, or no loans toward it never
Feature flag off never

On MyKiva a ?goTo=goal-recap deep link returns early and skips the auto-open, so an explicit link does not silently consume the once-only flag. The deep link also does not set the flag, so a link in an email keeps working however many times it is clicked.


Test instructions - VQA

You need:

  • goal_in_review_enable switched on.
  • A logged in lender with a goal for the current year and at least one loan toward it.

Use /mykiva or /portfolio with no query parameters. A ?goTo=goal-recap parameter skips the auto-open path deliberately.

Check your starting state

Run this in the gateway while logged in as the test account:

query { my { id userPreferences { id preferences } } }

Look inside the preferences JSON for goalRecapViewed and goalRecapPending. They determine what happens next, and they are stored on the account (clearing cookies or changing browser will not reset them).

Scenarios

In progress goal

  1. No flags set. Visit /mykiva -> the recap opens.
  2. Reload -> nothing. goalRecapViewed[year] is now set.
  3. Visit /portfolio -> nothing. This is the "once in total, not once per page".

Completed goal

  1. No flags set. Visit /mykiva -> nothing shown. This is the session they completed in. goalRecapPending[year] is now set.
  2. Visit /mykiva or /portfolio again -> the recap opens.
  3. Anywhere after that -> nothing.

Portfolio page, without visiting MyKiva

Worth running as its own pass. The completed-goal rule is driven by goalRecapPending[year], which either page can set, so a lender who only ever visits Portfolio gets the same two-step behaviour as one who uses MyKiva:

  1. Completed goal, no flags. Visit /portfolio → nothing, arms the marker.
  2. Visit /portfolio again -> opens.
  3. Visit /mykiva -> nothing.

Dismissal counts as seen

Open the recap, then close it with the X or by clicking outside it. Reload -> it does not come back. The flag is written when it opens, not when it is read.

Cases that must never pop

  • A goal from a previous year, or an expired goal.
  • A lender with no goal, or a goal with no loans toward it.
  • Anyone, with goal_in_review_enable off.

Resetting between runs

The flags are written to the account, so each scenario is one-shot. To run them again, clear goalRecapViewed and goalRecapPending from that user's preferences.

@christian14b
christian14b requested a review from a team August 7, 2026 16:36
@christian14b christian14b added the b2c Sends B2C team a message in Slack on PR creation label Aug 7, 2026
GoalEntrypoint
},
setup() {
const apollo = inject('apollo');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is there a way to avoid injecting apollo twice?

@christian14b christian14b Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Interesting question, I'll take a look

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Also, useGoalInReview({ apollo }) spins up a useGoalData internally, and setup creates a second one directly. Since userPreferences is a per-call ref(null) rather than module-scoped, the instance loadAutoOpenRecap writes goalRecapViewed/goalRecapPending into is a different reactive store from the one the page reads feedback state from. It works today only because everything reloads network-only, but it's a duplicate round-trip and a divergence trap if anything later leans on shared reactive prefs. Could we thread one useGoalData through, or have useGoalInReview re-expose the feedback helpers the page needs? (MyKiva has the same shape.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch! and it was worse than two. MyKiva had three useGoalData instances: the page-wide one MyKivaPage provides, the one MyKivaPageContent was injecting, and the one useGoalInReview creates internally. Portfolio had two.

Indeed, userPreferences is ref(null) inside the composable, so each call gets its own store, and it only worked because every read forced network-only.

I will fix it by letting useGoalInReview accept an existing instance

Comment thread src/composables/useGoalData.js Outdated
if (!year) return;
const parsedPrefs = await loadPreferences('network-only');
const prev = parsedPrefs?.goalRecapViewed || {};
// Year-keyed flag so seeing this year's recap does not suppress next year's.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't know if this comment is accurate.

Comment thread src/composables/useGoalData.js Outdated

const goalRecapPendingByYear = computed(() => {
const parsedPrefs = JSON.parse(userPreferences.value?.preferences || '{}');
return parsedPrefs.goalRecapPending || {};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There are two methods here that are really similar and could be combined and passed a key.

Comment thread src/composables/useGoalData.js Outdated
});

function hasGoalRecapPendingForYear(year) {
return Boolean(goalRecapPendingByYear.value?.[year]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Similarly this method could be combined/abstracted.

Comment thread src/composables/useGoalData.js Outdated

// Set the first time a completed goal is seen, so the recap can open on the session
// after completion without depending on the goal card's own celebration flag.
async function setGoalRecapPendingPreference(year = GOALS_CURRENT_YEAR) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Another method that is almost duplicated above.

Comment thread src/util/goalInReviewTrigger.js Outdated
// In progress goal setters are reached when the feature itself goes live, so the
// flag is the only gate.
if (goalStatus === GOAL_STATUS.IN_PROGRESS) {
return true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This means any in-progress goal is shown in the new modal after the feature is enabled?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, called out in the epic:

Lenders who still have a goal in progress - show on log in as a pop up when they visit MyKiva or the Portfolio mid November just pop up once (total, not once per page)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

That mentions "mid November" instead of when the feature is enabled. Is that "November" check already in place for in-progress goals?

return;
}
await this.loadGoalPreferences('network-only');
this.goalInReviewFeedbackSubmitted = this.hasSubmittedGoalFeedbackForYear(goalInReview.year);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why is goalInReviewFeedbackSubmitted set here?

hasCompletionPending,
});

if (!shouldOpen) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why are we exiting if the user just completed a goal? Wouldn't this be true when returning to MyKiva after checking out the last loan? And I assume we'd want to show the new modal then?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's called out in the epic too: Lenders who completed their goal - the next session after they complete their goal, show as a pop up when they visit MyKiva or the Portfolio , but I'll reconfirm it asking in the channel

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ah, so first load on MyKiva sets the pending, second load shows?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

b2c Sends B2C team a message in Slack on PR creation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants