DISC-386 && Disc-388 && Disc-392: Open Calls filter Network layer - #2552
Conversation
Adds the backend plumbing for the Open Calls (Creative Prompt tag) search filter, ahead of the UI and deep-link PRs. Gated behind the new StatsigGateKey.ANDROID_OPEN_CALLS. Network / data: - New Tag model (Parcelable, id decoded from Relay ID via decodeRelayId). - New GetCreativePromptTags GraphQL query (tags scoped to CREATIVE_PROMPT). - FetchProjects gains a $tagId: Int variable; buildFetchProjectsQuery() forwards DiscoveryParams.tagId() via a new T?.toOptional() helper. - getTags(): Result<List<Tag>> on ApolloClientTypeV2 + KSApolloClientV2 (mirrors getCategories()), plus MockApolloClientV2 override and TagFactory. ViewModel / gate: - FilterMenuViewModel owns a tagsList via a getTags() loader mirroring getRootCategories(); FilterMenuUIState gains tagsList. - SearchAndFilterViewModel.updateParamsToSearchWith() gains tagId: Int?, applied through DiscoveryParams.Builder.tagId(). - isOpenCallsEnabled StateFlow derived from ANDROID_OPEN_CALLS using the configReady + EvalReason.Unrecognized pattern (mirrors isVideoFeedBannerVisible), not the one-shot isReady. Tests: getTags success/error, updateParamsToSearchWith(tagId), and gate on/off. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds network + viewmodel support for an “Open Calls / Creative Prompt” filter in Search, including fetching Creative Prompt tags, passing a tagId into the projects search query, and gating UI exposure via Statsig.
Changes:
- Extend
FetchProjectsGraphQL query +KSApolloClientV2to accept/send an optionaltagIddiscovery parameter. - Add a new
Tagmodel plusgetTags()API (GraphQLGetCreativePromptTags) and surface tags viaFilterMenuViewModel. - Add Statsig gate
ANDROID_OPEN_CALLSand expose it viaSearchAndFilterViewModel; add/extend unit tests for the new behaviors.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| app/src/test/java/com/kickstarter/features/search/viewmodel/SearchAndFilterViewModelTest.kt | Adds test coverage for tagId search param updates and the Open Calls Statsig gate state. |
| app/src/test/java/com/kickstarter/features/search/viewmodel/FilterMenuViewModelTest.kt | Adds tests for getTags() success/error flows and UI state updates. |
| app/src/main/java/com/kickstarter/services/KSApolloClientV2.kt | Adds getTags() implementation and wires tagId into FetchProjectsQuery variables. |
| app/src/main/java/com/kickstarter/models/Tag.kt | Introduces a Parcelable Tag model (id/name/url/slug) for Creative Prompt tags. |
| app/src/main/java/com/kickstarter/mock/services/MockApolloClientV2.kt | Extends mock client to support getTags() for tests. |
| app/src/main/java/com/kickstarter/mock/factories/TagFactory.kt | Adds factory helpers for generating tag fixtures in tests. |
| app/src/main/java/com/kickstarter/libs/featureflag/StatsigClient.kt | Adds ANDROID_OPEN_CALLS gate key. |
| app/src/main/java/com/kickstarter/features/search/viewmodel/SearchAndFilterViewModel.kt | Adds isOpenCallsEnabled StateFlow and tagId support in updateParamsToSearchWith. |
| app/src/main/java/com/kickstarter/features/search/viewmodel/FilterMenuViewModel.kt | Adds tagsList to UI state and a getTags() fetch method. |
| app/src/main/java/com/kickstarter/features/search/ui/SearchAndFilterActivity.kt | Triggers tag fetch on activity creation. |
| app/src/main/graphql/tags.graphql | Adds GetCreativePromptTags query. |
| app/src/main/graphql/project.graphql | Adds $tagId variable and tagId argument to FetchProjects query. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| filterMenuViewModel.getRootCategories() | ||
| filterMenuViewModel.getTags() | ||
|
|
There was a problem hiding this comment.
Follow up PR wil hide/show the UI using the gate, can comment out this piece, is as of now more to facilitate QA.
| * Collapses the repeated `if (x == null) Optional.absent() else Optional.present(x)` pattern | ||
| * used when building Apollo query variables. | ||
| */ | ||
| private fun <T : Any> T?.toOptional(): Optional<T> = |
There was a problem hiding this comment.
This could be applied broadly on this entire file, will do so on follow up PR if you agree @tonyteate
There was a problem hiding this comment.
Nice! Yep, honestly surprised this isn't built-in.
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #2552 +/- ##
============================================
+ Coverage 65.69% 65.71% +0.01%
- Complexity 2558 2563 +5
============================================
Files 402 403 +1
Lines 32098 32159 +61
Branches 4562 4569 +7
============================================
+ Hits 21088 21133 +45
- Misses 8595 8606 +11
- Partials 2415 2420 +5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| } | ||
|
|
||
| scope.launch { | ||
| statsigClient.configReady.collect { configReady -> |
There was a problem hiding this comment.
Do we want to collect configReady here continuously? Could useful to do a filter or one-time check e.g. statsigClient.configReady.first { it }.
There was a problem hiding this comment.
I do not expect this piece to be updated unless a change on the internal statsig user state, or changes on the rollout state. I wanna avoid the flickering we saw with VideoFeed, granted I could add some logging to verify once released really how many times this is called.
Given this specific new filter is not highly visible I could see too collecting just once, maybe I'm playing overly safe here.
There was a problem hiding this comment.
Gotcha! Would be curious to see the logging if you end up doing it. The previous case seemed like a special case, but if we have to re-use this pattern, we might want to revisit the custom cache key. Ideally, if I'm understanding the position of the documentation, accessing Statsig should be "transparent" -- we can always get a value, change the UX based on the value, but should not have to think about when or where we get that value.
| fun slug() = this.slug | ||
|
|
||
| @Parcelize | ||
| data class Builder( |
There was a problem hiding this comment.
- With
Tagas a data class with a public constructor, is the Builder still necessary? - If we keep the Builder, I'd argue that it should not be Parcelable.
There was a problem hiding this comment.
Noted, Will change to a data class on follow up PR!
| .id(1L) | ||
| .name("Make 100") | ||
| .slug("make-100") | ||
| .url("https://www.kickstarter.com/discover/advanced?tag_id=1") |
There was a problem hiding this comment.
Super-nit for clarity: I think the urls here are of the form /discover/tags/<tags> (though I know it's tag_id when used a search param)
📲 What
Adds the network + ViewModel + feature-gate foundation for the Open Calls (Creative Prompt) search filter:
GetCreativePromptTagsGraphQL query and aTagmodel (id/name/url/slug).KSApolloClientV2.getTags()to fetch Creative Prompt tags.tagIdthreaded into theFetchProjectsquery andDiscoveryParams, so a selected tag scopes search results.FilterMenuViewModel.getTags()exposes the tag list via FilterMenuUIState.tagsList, loaded on screen entry.SearchAndFilterViewModel.isOpenCallsEnabled, gated on the newandroid_open_callsStatsig gate.🤔 Why
Open Calls lets backers filter search to projects participating in a Creative Prompt (Make 100, Zine Quest, etc.). This is the first of the chained PRs — it lands the data/gate layer so the UI PR can build on top without a giant single diff.
🛠 How
tags.graphql: new query scoped to CREATIVE_PROMPT. Tag.id is a Long decoded from the Relay ID!; converted with id.toInt() at the DiscoveryParams.tagId (Int) boundary.project.graphql: added $tagId: Int variable, passed to projects(tagId:).SearchAndFilterViewModel.isOpenCallsEnabledwill be consumed to hide/show the UI piece in follow up PR's.👀 See

📋 QA
No user-facing UI yet, but use Network Profiler to check the newly added query response. Unit test coverage added in FilterMenuViewModelTest and SearchAndFilterViewModelTest.
Story 📖
DISC-386
DISC-388
DISC-392