Skip to content

feat(mobile-ios): GraphQL client and codegen#116

Merged
Ur-imazing merged 19 commits intomainfrom
feat/102-graphql
Feb 26, 2026
Merged

feat(mobile-ios): GraphQL client and codegen#116
Ur-imazing merged 19 commits intomainfrom
feat/102-graphql

Conversation

@Ur-imazing
Copy link
Copy Markdown
Contributor

@Ur-imazing Ur-imazing commented Feb 25, 2026

Resolves #102

Summary

Adds Apollo iOS and codegen for the iOS app: schema from apps/cms/schema.graphql, one query (GetWatchExperience), generated Swift in ForgeMobile, and a GraphQLContentClient that implements ContentClient with configurable endpoint and optional bearer token. Enables the app to fetch watch experience data from the CMS GraphQL API.

Contracts Changed

  • yes
  • no

Regeneration Required

  • yes
  • no

Validation

  • Contracts validated
  • Generated code verified (no manual edits)
  • Tests and build passed (swift build from mobile/ios)
  • Terraform plan reviewed (if infra change) — N/A

Summary by CodeRabbit

  • New Features

    • Integrated GraphQL support for fetching content dynamically from a content management system.
  • Documentation

    • Added setup guides for GraphQL configuration, code generation, and local development with environment-specific endpoints and token authentication.
  • Chores

    • Added required dependencies and build configurations for GraphQL support.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)

Walkthrough

This PR integrates Apollo iOS as the GraphQL client for iOS, adding schema codegen configuration, a GraphQLContentClient to fetch Experience data from Strapi, Strapi token management via build phases, and updates the app structure to support configurable endpoints and authentication for dev/prod environments.

Changes

Cohort / File(s) Summary
GraphQL Dependencies & Configuration
mobile/ios/Package.swift, mobile/ios/Package.resolved, mobile/ios/apollo-codegen-configuration.json
Adds Apollo iOS dependency (v1.25.3) to Package.swift, documents resolved package pins, and configures Apollo codegen with schema/operation search paths and output targets for ForgeSchema namespace.
GraphQL Operations & Client
mobile/ios/GraphQL/Operations/GetWatchExperience.graphql, mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift
Defines GetWatchExperience query accepting locale and filters, and implements GraphQLContentClient to execute queries with Apollo, map responses to MobileContentItem, and surface GraphQL errors via custom error type.
Content Integration & Rendering
mobile/ios/Sources/ForgeMobile/ContentRepository.swift, mobile/ios/Sources/ForgeMobile/ForgeRootView.swift
Updates ContentRepository to fetch "experience" slug instead of "home"; injects ContentRepository into ForgeRootView via initializer and conditionally renders GraphQLTestView for content fetching and display.
App Configuration & Token Generation
mobile/ios/App/ForgeApp/ForgeApp.swift, mobile/ios/App/ForgeApp.xcodeproj/project.pbxproj, mobile/ios/App/ForgeApp/Info-Debug.plist, mobile/ios/App/ForgeApp/Info-Release.plist
Adds static graphQLURL property reading from Info.plist with localhost fallback for debug; implements makeContentRepository() to configure GraphQLContentClient with bearer token; introduces build phase to generate StrapiToken.swift from environment; adds Debug/Release plist configurations with GraphQL endpoints.
Generated Code & Build Hygiene
mobile/ios/.gitignore, mobile/ios/.swiftlint.yml, .gitignore
Ignores Apollo iOS CLI symlink and generated Strapi token file; excludes Sources/ForgeMobile/Generated and App/ForgeApp/Generated from SwiftLint checks.
Documentation & Environment
mobile/ios/README.md, apps/cms/.env.example
Documents GraphQLContentClient usage, Strapi token configuration (debug vs. release behavior), Apollo codegen setup, and schema/operations locations; adds STRAPI_FULL_ACCESS_TOKEN to CMS environment example.

Sequence Diagram

sequenceDiagram
    actor User
    participant ForgeRootView
    participant ContentRepository
    participant GraphQLContentClient
    participant Apollo
    participant Strapi

    User->>ForgeRootView: App loads
    ForgeRootView->>ContentRepository: fetchHome(locale: "en")
    ContentRepository->>GraphQLContentClient: getContent(locale: "en", slug: "experience")
    GraphQLContentClient->>GraphQLContentClient: Build GetWatchExperience query
    GraphQLContentClient->>Apollo: Execute GraphQL query
    Apollo->>Strapi: POST /graphql (with bearer token)
    Strapi-->>Apollo: Experience data + sections
    Apollo-->>GraphQLContentClient: Query result
    GraphQLContentClient->>GraphQLContentClient: Map experience to MobileContentItem
    GraphQLContentClient-->>ContentRepository: MobileContentItem
    ContentRepository-->>ForgeRootView: Content result
    ForgeRootView->>ForgeRootView: Render GraphQLTestView with content
    ForgeRootView-->>User: Display fetched content
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • JesusFilm/forge#18: Updates mobile/ios/.swiftlint.yml exclusions for generated code in the same locations.
  • JesusFilm/forge#112: Modifies overlapping iOS scaffold files (ForgeApp.xcodeproj, ForgeApp.swift, ForgeRootView) with potential conflicts in build configuration and app initialization.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(mobile-ios): GraphQL client and codegen' accurately describes the main change: introducing Apollo iOS GraphQL client and code generation for the mobile iOS app.
Linked Issues check ✅ Passed The PR satisfies all acceptance criteria from #102: codegen consumes apps/cms/schema.graphql, generates types for Experience and section types, implements GetWatchExperience query, and provides GraphQLContentClient with endpoint and bearer token support.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the GraphQL client and codegen for iOS as specified in #102; no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/102-graphql

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Ur-imazing Ur-imazing changed the title feat(ios): GraphQL client and codegen feat(mobile-ios): GraphQL client and codegen Feb 25, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift (2)

33-37: Consider simplifying the withCheckedContinuation verbose type annotation.

The explicit (continuation: CheckedContinuation<Result<GraphQLResult<ForgeSchema.GetWatchExperienceQuery.Data>, Error>, Never>) annotation is very long. Type inference handles this automatically when you declare the result variable inline.

♻️ Suggested simplification
-    let result = await withCheckedContinuation { (continuation: CheckedContinuation<Result<GraphQLResult<ForgeSchema.GetWatchExperienceQuery.Data>, Error>, Never>) in
+    let result = await withCheckedContinuation { continuation in
       apollo.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { result in
         continuation.resume(returning: result)
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift` around lines 33 -
37, The continuation closure currently includes a very long explicit type
((continuation:
CheckedContinuation<Result<GraphQLResult<ForgeSchema.GetWatchExperienceQuery.Data>,
Error>, Never>))—remove that annotation and let Swift infer the type by writing
withCheckedContinuation { continuation in ... } (keep the apollo.fetch call and
continuation.resume(returning: result) as-is) so the code uses type inference
for withCheckedContinuation.

61-61: Remove redundant String() initializer on documentId

Apollo iOS maps the GraphQL ID scalar to Swift String via typealias, so documentId is already a String. The String(...) call is unnecessary.

♻️ Suggested cleanup
-      id: String(experience.documentId),
+      id: experience.documentId,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift` at line 61, In
GraphQLContentClient.swift replace the redundant String(...) wrapper around the
GraphQL ID by passing experience.documentId directly (remove
String(experience.documentId) and use experience.documentId for the id field)
since Apollo maps ID to Swift String; update any nearby code that constructs the
same id field in the same initializer or function to follow the same pattern.
mobile/ios/GraphQL/Operations/GetWatchExperience.graphql (1)

9-9: __typename is redundant — Apollo iOS injects it automatically.

Apollo iOS's codegen unconditionally emits __typename on every type it processes, so this explicit selection is a no-op. It's harmless but can be removed to reduce noise.

♻️ Suggested cleanup
   sections {
-    __typename
     ... on ComponentSectionsMediaCollection {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/ios/GraphQL/Operations/GetWatchExperience.graphql` at line 9, Remove
the explicit "__typename" field selection from the GetWatchExperience GraphQL
operation (GetWatchExperience.graphql) because Apollo iOS codegen already
injects __typename; update the operation definition to omit the __typename line
so the generated code and query are cleaner and avoid redundant selections.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mobile/ios/apollo-codegen-configuration.json`:
- Line 4: The schemaSearchPaths entry in apollo-codegen-configuration.json
currently uses a relative path ("../../apps/cms/schema.graphql") that can break
when CI runs codegen from a different cwd; update CI to invoke apollo-ios-cli
generate from the mobile/ios directory or call the CLI with the
--project-directory option so the relative path is resolved correctly, or change
schemaSearchPaths to a path anchored to the repository root (or an environment
variable) so it does not depend on the working directory; locate and update the
"schemaSearchPaths" entry in apollo-codegen-configuration.json and adjust the CI
job that runs apollo-ios-cli accordingly.

In `@mobile/ios/Package.swift`:
- Line 6: Remove the macOS platform entry from the Package.swift platforms
declaration so the package is iOS-only; locate the platforms array (platforms:
[.iOS(.v17), .macOS(.v12)]) and change it to only include .iOS(.v17), removing
the .macOS(.v12) element and any trailing comma, and ensure no comments or other
references to macOS remain in the Package.swift or related package
configuration.

In `@mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift`:
- Around line 65-66: The MobileContentItem creation in
GraphQLContentClient.swift currently hardcodes body: "" and state: "published";
update the code in the mapping/construction logic (where MobileContentItem is
assembled) to extract body and publication state from the GraphQL response
fields (use the response property names the client receives) instead of
hardcoding, or if those fields are intentionally absent, replace the literals
with clear TODO comments documenting why they are stubbed and the expected
schema field names (e.g., body, state) so future callers don’t assume published
state; ensure you adjust any uses of MobileContentItem.state accordingly to
avoid silently treating drafts as published.
- Around line 76-78: In firstSectionTitle, the PromoBanner branch returns
promo.promoBannerHeading unconditionally
(section.asComponentSectionsPromoBanner), which can prematurely exit the loop
when heading is nil/empty; change this branch to only return when
promo.promoBannerHeading is non-nil and not empty (mirror other branches'
checks) so the loop continues to examine later sections and falls back to
experience.slug only if no valid title is found.

---

Nitpick comments:
In `@mobile/ios/GraphQL/Operations/GetWatchExperience.graphql`:
- Line 9: Remove the explicit "__typename" field selection from the
GetWatchExperience GraphQL operation (GetWatchExperience.graphql) because Apollo
iOS codegen already injects __typename; update the operation definition to omit
the __typename line so the generated code and query are cleaner and avoid
redundant selections.

In `@mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift`:
- Around line 33-37: The continuation closure currently includes a very long
explicit type ((continuation:
CheckedContinuation<Result<GraphQLResult<ForgeSchema.GetWatchExperienceQuery.Data>,
Error>, Never>))—remove that annotation and let Swift infer the type by writing
withCheckedContinuation { continuation in ... } (keep the apollo.fetch call and
continuation.resume(returning: result) as-is) so the code uses type inference
for withCheckedContinuation.
- Line 61: In GraphQLContentClient.swift replace the redundant String(...)
wrapper around the GraphQL ID by passing experience.documentId directly (remove
String(experience.documentId) and use experience.documentId for the id field)
since Apollo maps ID to Swift String; update any nearby code that constructs the
same id field in the same initializer or function to follow the same pattern.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c6c06d6 and eeedf0c.

⛔ Files ignored due to path filters (22)
  • mobile/ios/Sources/ForgeMobile/Generated/ForgeSchema.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Operations/Queries/GetWatchExperienceQuery.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/CustomScalars/DateTime.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/CustomScalars/I18NLocaleCode.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/CustomScalars/ID.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/Enums/ENUM_COMPONENTSECTIONSMEDIACOLLECTION_VARIANT.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/InputObjects/BooleanFilterInput.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/InputObjects/DateTimeFilterInput.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/InputObjects/ExperienceFiltersInput.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/InputObjects/IDFilterInput.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/InputObjects/StringFilterInput.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/Objects/ComponentSectionsCta.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/Objects/ComponentSectionsInfoBlock.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/Objects/ComponentSectionsInfoBlocks.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/Objects/ComponentSectionsMediaCollection.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/Objects/ComponentSectionsPromoBanner.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/Objects/Error.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/Objects/Experience.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/Objects/Query.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/SchemaConfiguration.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/SchemaMetadata.graphql.swift is excluded by !**/generated/**
  • mobile/ios/Sources/ForgeMobile/Generated/Schema/Unions/ExperienceSectionsDynamicZone.graphql.swift is excluded by !**/generated/**
📒 Files selected for processing (7)
  • mobile/ios/.gitignore
  • mobile/ios/GraphQL/Operations/GetWatchExperience.graphql
  • mobile/ios/Package.resolved
  • mobile/ios/Package.swift
  • mobile/ios/README.md
  • mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift
  • mobile/ios/apollo-codegen-configuration.json

Comment thread mobile/ios/apollo-codegen-configuration.json
Comment thread mobile/ios/Package.swift Outdated
Comment thread mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift Outdated
Comment thread mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift Outdated
@Ur-imazing
Copy link
Copy Markdown
Contributor Author

Review feedback addressed (2a3b753, e95cc9b)

Fixed:

  • Schema path / codegen cwd (apollo-codegen-configuration.json): README now states that codegen must be run from mobile/ios (relative path requirement) and that any CI/script must use that working directory.
  • macOS platform (Package.swift): Removed .macOS(.v12) so the package is iOS-only per project guidelines.
  • Stubbed body/state (GraphQLContentClient.swift): Added comments documenting that body and state are intentionally stubbed (Experience has no root-level body; publication state not in query; callers should not rely on state for access control).
  • PromoBanner title (GraphQLContentClient.swift): firstSectionTitle now returns promoBannerHeading only when non-empty, so the loop continues to later sections instead of exiting early.

Not changed: n/a — all four CodeRabbit comments were addressed.

@coderabbitai coderabbitai Bot added the tooling label Feb 25, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
.github/workflows/ci.yml (1)

107-110: Add env configuration to turbo.json for the build task to pass secrets via process environment instead of .env.local.

STRAPI_API_TOKEN is a backend secret that should not be written to disk in CI, as it risks exposure via Turbo's remote cache, CI caches, or build artifacts. Since the workflow already provides these as environment variables, declare them in turbo.json:

"build": {
  "dependsOn": ["^build"],
  "outputs": ["dist/**", ".next/**", "build/**"],
  "env": ["STRAPI_API_TOKEN", "NEXT_PUBLIC_GRAPHQL_URL"]
}

This allows Turborepo (in Strict Mode) to pass them through to next build without requiring a .env.local file. Next.js and Turborepo's framework inference will handle both backend and frontend variable usage correctly at build time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 107 - 110, Update the Turborepo build
task so CI secrets are passed via process env instead of writing to disk: add
the "env" array to the "build" task in turbo.json and include "STRAPI_API_TOKEN"
and "NEXT_PUBLIC_GRAPHQL_URL". Remove or stop using the step that echoes these
variables into apps/web/.env.local in the CI workflow (the echo lines in
.github/workflows/ci.yml) so the secrets are not written to disk; Turborepo will
forward the declared env vars to next build in strict mode.
mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift (3)

33-37: withCheckedContinuation is correct for Apollo iOS 1.0, but upgrading to 2.x enables native async/await.

Swift structured concurrency introduces an entirely new mental model for cancellation and isolation; Apollo iOS 2.0 leans into that world with a Sendable-aware client surface so GraphQL workflows compose naturally with tasks and cancellation semantics. In contrast, in Apollo iOS 1.0, RequestChain was a protocol with a provided implementation InterceptorRequestChain and fetch is strictly callback-based — meaning the current withCheckedContinuation bridging is the correct approach for the pinned 1.0.0 version. There is no native async overload to call.

The trade-off to be aware of: if the calling Task is cancelled while awaiting, the underlying Apollo 1.x HTTP request is not cancelled — it completes in the background and the continuation simply resumes with the result. This is benign but wasteful.

Consider tracking an upgrade to Apollo iOS 2.x (which exposes try await apollo.fetch(query:) natively) so this bridging wrapper and its cancellation gap can be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift` around lines 33 -
37, The current GraphQLContentClient.swift bridges the callback-based
apollo.fetch using withCheckedContinuation (wrapping apollo.fetch) which works
for Apollo iOS 1.x but leaves a cancellation gap where cancelling the calling
Task does not cancel the underlying HTTP request; fix by upgrading to Apollo iOS
2.x and replacing the continuation wrapper with the native async API (call try
await apollo.fetch(query:) directly in the GraphQLContentClient method, convert
the method signature to async/throws as needed, remove the
withCheckedContinuation block and associated callback handling, and ensure the
client code uses the Sendable-aware Apollo client surface so Task cancellation
propagates).

63-63: Remove the redundant String() cast on experience.documentId.

ForgeSchema.ID is a typealias for String, making String(experience.documentId) a no-op conversion. Change to:

Diff
      id: String(experience.documentId),

to

      id: experience.documentId,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift` at line 63, The id
assignment using String(experience.documentId) is a redundant cast because
ForgeSchema.ID is a String typealias; update the id initializer in
GraphQLContentClient (the assignment that uses experience.documentId) to pass
experience.documentId directly (remove the String(...) wrapper) so the field
uses the existing String value without re-casting.

63-63: Remove redundant String() conversion on documentId.

ForgeSchema.ID is a typealias for String, so experience.documentId is already String. The String(…) wrapper is a no-op cast—remove it to simplify the code:

id: experience.documentId,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift` at line 63, The id
field is being wrapped in an unnecessary String(...) when ForgeSchema.ID is
already a typealias for String; locate the initializer/assignment where you set
id: String(experience.documentId) in GraphQLContentClient.swift and replace it
with the direct value id: experience.documentId (remove the String(...) wrapper)
so the code uses the existing String-typed ForgeSchema.ID without a redundant
conversion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 108-110: The CI step currently appends environment variables to
apps/web/.env.local using >> which can create duplicate keys; change the two
echo commands that write STRAPI_API_TOKEN and NEXT_PUBLIC_GRAPHQL_URL to use >
(overwrite) for the first write and >> (append) for the second, or simply write
both into a fresh file in one overwrite command so apps/web/.env.local is
recreated cleanly before pnpm build; update the echo invocations in the workflow
block that currently read echo "STRAPI_API_TOKEN=$STRAPI_API_TOKEN" and echo
"NEXT_PUBLIC_GRAPHQL_URL=$NEXT_PUBLIC_GRAPHQL_URL" accordingly.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 107-110: Update the Turborepo build task so CI secrets are passed
via process env instead of writing to disk: add the "env" array to the "build"
task in turbo.json and include "STRAPI_API_TOKEN" and "NEXT_PUBLIC_GRAPHQL_URL".
Remove or stop using the step that echoes these variables into
apps/web/.env.local in the CI workflow (the echo lines in
.github/workflows/ci.yml) so the secrets are not written to disk; Turborepo will
forward the declared env vars to next build in strict mode.

In `@mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift`:
- Around line 33-37: The current GraphQLContentClient.swift bridges the
callback-based apollo.fetch using withCheckedContinuation (wrapping
apollo.fetch) which works for Apollo iOS 1.x but leaves a cancellation gap where
cancelling the calling Task does not cancel the underlying HTTP request; fix by
upgrading to Apollo iOS 2.x and replacing the continuation wrapper with the
native async API (call try await apollo.fetch(query:) directly in the
GraphQLContentClient method, convert the method signature to async/throws as
needed, remove the withCheckedContinuation block and associated callback
handling, and ensure the client code uses the Sendable-aware Apollo client
surface so Task cancellation propagates).
- Line 63: The id assignment using String(experience.documentId) is a redundant
cast because ForgeSchema.ID is a String typealias; update the id initializer in
GraphQLContentClient (the assignment that uses experience.documentId) to pass
experience.documentId directly (remove the String(...) wrapper) so the field
uses the existing String value without re-casting.
- Line 63: The id field is being wrapped in an unnecessary String(...) when
ForgeSchema.ID is already a typealias for String; locate the
initializer/assignment where you set id: String(experience.documentId) in
GraphQLContentClient.swift and replace it with the direct value id:
experience.documentId (remove the String(...) wrapper) so the code uses the
existing String-typed ForgeSchema.ID without a redundant conversion.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between eeedf0c and da2260a.

⛔ Files ignored due to path filters (1)
  • mobile/ios/Sources/ForgeMobile/Generated/Operations/Queries/GetWatchExperienceQuery.graphql.swift is excluded by !**/generated/**
📒 Files selected for processing (5)
  • .github/workflows/ci.yml
  • mobile/ios/GraphQL/Operations/GetWatchExperience.graphql
  • mobile/ios/Package.swift
  • mobile/ios/README.md
  • mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • mobile/ios/Package.swift
  • mobile/ios/README.md
  • mobile/ios/GraphQL/Operations/GetWatchExperience.graphql

Comment thread .github/workflows/ci.yml Outdated
@Ur-imazing Ur-imazing self-assigned this Feb 25, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift`:
- Around line 18-22: The current headers assignment in
GraphQLContentClient.swift sets Authorization even for blank bearerToken; change
the conditional that builds headers to require a non-empty, non-whitespace token
(e.g., guard let token = bearerToken, !token.trimmingCharacters(in:
.whitespacesAndNewlines).isEmpty) before adding ["Authorization": "Bearer
\(token)"], otherwise return an empty dictionary; update the code that defines
headers (the let headers ... using bearerToken) to perform this check so blank
tokens do not produce an Authorization header.
- Around line 49-55: The success case in the response handling (case
.success(let graphQLResult) inside GraphQLContentClient) only throws GraphQL
errors when data is nil, which lets responses with partial data and errors slip
through; update the logic to check graphQLResult.errors first (if let errors =
graphQLResult.errors, !errors.isEmpty) and throw
GraphQLContentClientError.graphQLErrors(errors) even when graphQLResult.data is
non-nil, then proceed to guard-let data from graphQLResult.data and return it as
before.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between da2260a and 20ac8a9.

📒 Files selected for processing (1)
  • mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift

Comment thread mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift Outdated
Comment thread mobile/ios/Sources/ForgeMobile/Data/GraphQLContentClient.swift
… error text

- ForgeRootView: optional ContentRepository, fetch home on appear, show result/error
- ForgeApp: GraphQL client with token from env or build-generated StrapiToken.swift
- Run Script: read STRAPI_FULL_ACCESS_TOKEN from apps/cms/.env, write Generated/StrapiToken.swift
- GraphQLContentClientError: LocalizedError with real Strapi error message
- Info.plist: ATS exception for localhost; gitignore Generated/

Made-with: Cursor
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift (2)

18-22: ⚠️ Potential issue | 🟡 Minor

Skip Authorization when token is empty/whitespace.

Line 18 currently adds bearer auth for any non-nil token, including blank values.

🛠️ Suggested fix
-    let headers: [String: String] = if let token = bearerToken {
-      ["Authorization": "Bearer \(token)"]
+    let trimmedToken = bearerToken?.trimmingCharacters(in: .whitespacesAndNewlines)
+    let headers: [String: String] = if let token = trimmedToken, !token.isEmpty {
+      ["Authorization": "Bearer \(token)"]
     } else {
       [:]
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift` around lines 18 -
22, The current headers construction in GraphQLContentClient adds an
Authorization header for any non-nil bearerToken including empty or
whitespace-only strings; change the logic that builds headers (the let headers:
[String: String] assignment that references bearerToken) to trim whitespace and
only include ["Authorization": "Bearer \(token)"] when
token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty is false,
otherwise return an empty dictionary. Ensure you reference bearerToken and the
headers binding so the header is entirely skipped for nil/blank tokens.

49-55: ⚠️ Potential issue | 🟠 Major

Throw GraphQL errors even when data is present.

Line 50 only throws graphQLErrors when data is nil; GraphQL responses can contain partial data plus errors, which should not be silently accepted.

🛠️ Suggested fix
     switch result {
     case .success(let graphQLResult):
-      guard let data = graphQLResult.data else {
-        if let errors = graphQLResult.errors, !errors.isEmpty {
-          throw GraphQLContentClientError.graphQLErrors(errors)
-        }
-        return nil
-      }
+      if let errors = graphQLResult.errors, !errors.isEmpty {
+        throw GraphQLContentClientError.graphQLErrors(errors)
+      }
+      guard let data = graphQLResult.data else { return nil }
       guard let first = data.experiences.compactMap({ $0 }).first else {
         return nil
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift` around lines 49 -
55, The GraphQL response handling in GraphQLContentClient currently only throws
graphQLErrors when data == nil; update the success branch that handles
graphQLResult in the response parsing (the case .success(let graphQLResult)
block) to check graphQLResult.errors even when data is non-nil and throw
GraphQLContentClientError.graphQLErrors(errors) if errors exist and are not
empty, before returning the partial data; keep the existing behavior of
returning nil only when data is nil and there are no errors.
🧹 Nitpick comments (2)
mobile/ios/App/ForgeApp/Info.plist (1)

5-15: Limit ATS localhost exception to Debug builds only.

Line 5–15 currently applies cleartext localhost traffic allowance to Release too (same plist for all configs). Prefer a Debug-only plist (or config-gated ATS setting) so production builds don’t carry this exception.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/ios/App/ForgeApp/Info.plist` around lines 5 - 15, The Info.plist
currently grants NSExceptionAllowsInsecureHTTPLoads for localhost globally;
restrict this to Debug by creating a Debug-only plist or by moving the
NSAppTransportSecurity/NSExceptionDomains/localhost entry into a Debug-specific
file (e.g., Info-Debug.plist) and configuring the Xcode target to use that plist
for the Debug build configuration (or alternatively inject the
NSExceptionAllowsInsecureHTTPLoads key via a Debug xcconfig/Build Settings plist
merge), ensuring the main Info.plist used for Release does not contain
NSAppTransportSecurity/NSExceptionAllowsInsecureHTTPLoads for localhost; update
references to the symbols NSAppTransportSecurity, NSExceptionDomains, localhost,
and NSExceptionAllowsInsecureHTTPLoads when making the change.
mobile/ios/Sources/ForgeMobile/ForgeRootView.swift (1)

11-13: Gate GraphQLTestView behind a debug/feature flag.

Because ForgeApp always injects a repository, this path becomes the default runtime UI. Prefer #if DEBUG or an explicit feature flag for this temporary validation surface.

Also applies to: 20-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/ios/Sources/ForgeMobile/ForgeRootView.swift` around lines 11 - 13, The
GraphQLTestView is always shown because ForgeApp always injects
contentRepository; gate its rendering in ForgeRootView by adding a
debug/feature-flag check around the GraphQLTestView usage (either wrap the
repository branch that returns GraphQLTestView inside a compile-time `#if` DEBUG
block or consult an explicit feature flag boolean like isGraphQLTestEnabled
injected into ForgeRootView/environment) and fall back to the existing
production UI when the flag is false; update both the initial occurrence and the
repeated block spanning lines 20–57 so only when DEBUG or isGraphQLTestEnabled
is true will GraphQLTestView(repository: repo) be created.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mobile/ios/App/ForgeApp.xcodeproj/project.pbxproj`:
- Around line 140-145: The build currently generates and compiles a
high-privilege credential into the app via the "Generate Strapi token" build
phase that outputs StrapiToken.swift and then includes it in the target; remove
this build phase and stop compiling StrapiToken.swift, and instead switch to a
runtime-safe approach: have the app fetch short-lived or scoped tokens from a
secure backend endpoint (or use Keychain/secure enclave for tokens obtained at
runtime), ensure the code that referenced StrapiToken.swift is updated to call
the runtime token provider, and add a lint/check to prevent regenerating/adding
any apps/* env secrets into mobile/** in future.

In `@mobile/ios/App/ForgeApp/ForgeApp.swift`:
- Line 6: Replace the hardcoded static URL by loading the GraphQL endpoint from
app configuration (Info.plist / build configs) and handling it safely: read
Bundle.main.object(forInfoDictionaryKey:) for a key like "GraphQLEndpoint",
validate and create a URL (avoid force-unwrapping), provide a sensible fallback
or fatal error with a clear message for missing/invalid values, and use that URL
when initializing GraphQLContentClient (update the static let graphQLURL and the
GraphQLContentClient initialization sites referenced in ForgeApp.swift lines
~14-19 to use the configured value).

---

Duplicate comments:
In `@mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift`:
- Around line 18-22: The current headers construction in GraphQLContentClient
adds an Authorization header for any non-nil bearerToken including empty or
whitespace-only strings; change the logic that builds headers (the let headers:
[String: String] assignment that references bearerToken) to trim whitespace and
only include ["Authorization": "Bearer \(token)"] when
token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty is false,
otherwise return an empty dictionary. Ensure you reference bearerToken and the
headers binding so the header is entirely skipped for nil/blank tokens.
- Around line 49-55: The GraphQL response handling in GraphQLContentClient
currently only throws graphQLErrors when data == nil; update the success branch
that handles graphQLResult in the response parsing (the case .success(let
graphQLResult) block) to check graphQLResult.errors even when data is non-nil
and throw GraphQLContentClientError.graphQLErrors(errors) if errors exist and
are not empty, before returning the partial data; keep the existing behavior of
returning nil only when data is nil and there are no errors.

---

Nitpick comments:
In `@mobile/ios/App/ForgeApp/Info.plist`:
- Around line 5-15: The Info.plist currently grants
NSExceptionAllowsInsecureHTTPLoads for localhost globally; restrict this to
Debug by creating a Debug-only plist or by moving the
NSAppTransportSecurity/NSExceptionDomains/localhost entry into a Debug-specific
file (e.g., Info-Debug.plist) and configuring the Xcode target to use that plist
for the Debug build configuration (or alternatively inject the
NSExceptionAllowsInsecureHTTPLoads key via a Debug xcconfig/Build Settings plist
merge), ensuring the main Info.plist used for Release does not contain
NSAppTransportSecurity/NSExceptionAllowsInsecureHTTPLoads for localhost; update
references to the symbols NSAppTransportSecurity, NSExceptionDomains, localhost,
and NSExceptionAllowsInsecureHTTPLoads when making the change.

In `@mobile/ios/Sources/ForgeMobile/ForgeRootView.swift`:
- Around line 11-13: The GraphQLTestView is always shown because ForgeApp always
injects contentRepository; gate its rendering in ForgeRootView by adding a
debug/feature-flag check around the GraphQLTestView usage (either wrap the
repository branch that returns GraphQLTestView inside a compile-time `#if` DEBUG
block or consult an explicit feature flag boolean like isGraphQLTestEnabled
injected into ForgeRootView/environment) and fall back to the existing
production UI when the flag is false; update both the initial occurrence and the
repeated block spanning lines 20–57 so only when DEBUG or isGraphQLTestEnabled
is true will GraphQLTestView(repository: repo) be created.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 20ac8a9 and 369ae3f.

📒 Files selected for processing (9)
  • .gitignore
  • apps/cms/.env.example
  • mobile/ios/App/ForgeApp.xcodeproj/project.pbxproj
  • mobile/ios/App/ForgeApp/ForgeApp.swift
  • mobile/ios/App/ForgeApp/Info.plist
  • mobile/ios/README.md
  • mobile/ios/Sources/ForgeMobile/ContentRepository.swift
  • mobile/ios/Sources/ForgeMobile/ForgeRootView.swift
  • mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (1)
  • mobile/ios/README.md

Comment thread mobile/ios/App/ForgeApp.xcodeproj/project.pbxproj Outdated
Comment thread mobile/ios/App/ForgeApp/ForgeApp.swift Outdated
- Info-Debug.plist: localhost HTTP exception for local Strapi (Debug only)
- Info-Release.plist: no ATS override for Archive / App Store Connect
- Project: INFOPLIST_FILE per configuration; README documents plist usage

Made-with: Cursor
- Sources/ForgeMobile/Generated (Apollo)
- App/ForgeApp/Generated (StrapiToken.swift)
so pnpm lint passes for @forge/mobile-ios

Made-with: Cursor
- GraphQLContentClient: only set Authorization when token non-empty (trim whitespace)
- GraphQLContentClient: throw graphQLErrors when errors present even if data exists
- GraphQLContentClient: use experience.documentId directly (ID is String)
- GetWatchExperience.graphql: remove redundant __typename (Apollo injects)
- ForgeRootView: show GraphQLTestView only in DEBUG builds
- ForgeApp: document localhost URL as default for local dev
- Run Script: omit token in Release so app binary never contains credential

Made-with: Cursor
@Ur-imazing
Copy link
Copy Markdown
Contributor Author

Review feedback addressed (cae66e8)

Fixed:

  • Blank token / Authorization header: Only set Authorization: Bearer when token is non-nil, trimmed and non-empty (avoids sending Bearer for blank tokens).
  • GraphQL errors with partial data: Success branch now checks graphQLResult.errors first and throws graphQLErrors even when data is present, then guards on data.
  • Redundant String(documentId): Use experience.documentId directly (ForgeSchema.ID is String).
  • Redundant __typename: Removed from GetWatchExperience.graphql (Apollo iOS injects it).
  • GraphQLTestView in production: Gated behind #if DEBUG; Release builds show plain "Forge iOS".
  • Localhost URL: Documented in ForgeApp as default for local dev; production should use a configured endpoint (future).
  • Token in binary (Critical): Run Script now writes kStrapiFullAccessToken = nil for Release; token is only read from apps/cms/.env in Debug. Archive/App Store builds never contain the credential. Removed .env from script inputPaths so Release does not depend on it.

Not changed:

  • Schema path / codegen cwd: Already documented in README; no CI step runs Apollo codegen; no config change (per existing reply on thread).
  • ci.yml .env.local: Current workflow has no echo >> .env.local step (that was reverted); nothing to fix.
  • Endpoint from Info.plist: Deferred; comment added that production should use configured endpoint.
  • Apollo 2.x / native async: Staying on 1.x for now; upgrade can be a follow-up.

… from plist

- Token phase: only emit nil stub; never read apps/cms/.env (Critical)
- ForgeApp: read GraphQLEndpoint from Info.plist; Debug fallback, Release requires value
- GraphQLContentClient: simplify withCheckedContinuation (nitpick)

Made-with: Cursor
@Ur-imazing
Copy link
Copy Markdown
Contributor Author

Review feedback addressed (383815e)

Fixed:

  • Critical (Strapi token in binary): Generate Strapi token build phase no longer reads apps/cms/.env. It only emits kStrapiFullAccessToken: String? = nil. Token must be supplied at runtime via STRAPI_FULL_ACCESS_TOKEN env (e.g. Xcode scheme or launch config).
  • Major (hardcoded GraphQL URL): ForgeApp reads GraphQLEndpoint from Info.plist. Added key to Info-Debug.plist (localhost) and Info-Release.plist (empty; production must set). Debug fallback remains localhost; Release fatalError if missing.
  • Nitpick: withCheckedContinuation simplified to continuation in (type inferred).

Not changed:

  • ci.yml .env.local: Current workflow uses job-level env: only and does not write .env.local; no change needed.
  • Apollo 2.x / cancellation: Left as-is; upgrade out of scope for this PR.
  • ATS localhost / GraphQLTestView gate: Optional nitpicks; can follow in a separate PR if desired.

Please mark the related review threads as resolved when satisfied.

…or runs

- Build phase reads STRAPI_FULL_ACCESS_TOKEN from apps/cms/.env in Debug only
- Release always emits nil so shipped binary has no token
- README: document token source and Forbidden access troubleshooting

Made-with: Cursor
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift (1)

41-45: Handle cooperative task cancellation in getContent.

The apollo.fetch() method returns a Cancellable that is not captured or managed. When a task calling getContent is cancelled, the fetch continues running in the background. Add cancellation checks to make the method cooperatively cancellable.

♻️ Proposed change
   public func getContent(locale: String, slug: String) async throws -> MobileContentItem? {
+    try Task.checkCancellation()
     let filters = ForgeSchema.ExperienceFiltersInput(
       slug: .some(ForgeSchema.StringFilterInput(eq: .some(slug)))
     )
@@
     let result = await withCheckedContinuation { continuation in
       apollo.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { result in
         continuation.resume(returning: result)
       }
     }
+    try Task.checkCancellation()
     switch result {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift` around lines 41 -
45, Capture the Cancellable returned by apollo.fetch in getContent and cancel it
when the Swift task is cancelled: wrap the await withCheckedContinuation block
in a withTaskCancellationHandler (or use withCheckedThrowingContinuation +
withTaskCancellationHandler) so the cancellation handler calls
cancellable.cancel(), store the result of apollo.fetch in a local var (e.g.,
cancellable) inside the continuation, and ensure you only resume the
continuation once (guard against double-resume) when the apollo callback
completes or when cancellation occurs; reference getContent, apollo.fetch,
withCheckedContinuation, and the Cancellable to locate and update the code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mobile/ios/Sources/ForgeMobile/ForgeRootView.swift`:
- Around line 25-63: The GraphQLTestView struct (and its async fetch logic /
ContentRepository dependency) must be wrapped in a compiler condition so it only
exists in debug builds: move the entire GraphQLTestView definition inside a `#if`
DEBUG ... `#endif` block (so GraphQLTestView, its `@State` properties and .task
async call are not compiled into Release). Also make ForgeRootView’s
contentRepository storage and initializer conditional (`#if` DEBUG) so release
builds don’t keep dead storage or init parameters: define contentRepository and
the init parameter only under `#if` DEBUG and provide a parameterless public init
in the `#else` branch.

---

Nitpick comments:
In `@mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift`:
- Around line 41-45: Capture the Cancellable returned by apollo.fetch in
getContent and cancel it when the Swift task is cancelled: wrap the await
withCheckedContinuation block in a withTaskCancellationHandler (or use
withCheckedThrowingContinuation + withTaskCancellationHandler) so the
cancellation handler calls cancellable.cancel(), store the result of
apollo.fetch in a local var (e.g., cancellable) inside the continuation, and
ensure you only resume the continuation once (guard against double-resume) when
the apollo callback completes or when cancellation occurs; reference getContent,
apollo.fetch, withCheckedContinuation, and the Cancellable to locate and update
the code.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 369ae3f and 06aa284.

📒 Files selected for processing (9)
  • mobile/ios/.swiftlint.yml
  • mobile/ios/App/ForgeApp.xcodeproj/project.pbxproj
  • mobile/ios/App/ForgeApp/ForgeApp.swift
  • mobile/ios/App/ForgeApp/Info-Debug.plist
  • mobile/ios/App/ForgeApp/Info-Release.plist
  • mobile/ios/GraphQL/Operations/GetWatchExperience.graphql
  • mobile/ios/README.md
  • mobile/ios/Sources/ForgeMobile/ForgeRootView.swift
  • mobile/ios/Sources/ForgeMobile/GraphQLContentClient.swift
✅ Files skipped from review due to trivial changes (1)
  • mobile/ios/App/ForgeApp/Info-Release.plist
🚧 Files skipped from review as they are similar to previous changes (4)
  • mobile/ios/GraphQL/Operations/GetWatchExperience.graphql
  • mobile/ios/App/ForgeApp/ForgeApp.swift
  • mobile/ios/App/ForgeApp.xcodeproj/project.pbxproj
  • mobile/ios/README.md

Comment thread mobile/ios/Sources/ForgeMobile/Views/ForgeRootView.swift
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
mobile/ios/Sources/ForgeMobile/ViewModels/WatchHomeViewModel.swift (1)

17-30: Minor: Redundant homeError = nil assignment on line 25.

Line 19 already sets homeError = nil at the start of load(). The assignment on line 25 after success is redundant.

Proposed simplification
     do {
       let item = try await repository.fetchHome(locale: locale)
       homeItem = item
-      homeError = nil
     } catch {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/ios/Sources/ForgeMobile/ViewModels/WatchHomeViewModel.swift` around
lines 17 - 30, In load(locale:) remove the redundant homeError = nil after a
successful fetch: keep the initial homeError = nil at the start of the method,
assign homeItem = item on success and only set homeError in the catch block;
update the success path in the load(locale:) function (which calls
repository.fetchHome) to drop the unnecessary homeError = nil assignment so the
state logic relies on the initial reset and the catch-only error assignment.
mobile/ios/App/ForgeApp/ForgeApp.swift (1)

4-11: Repository may be recreated on each body evaluation.

AppContentRepositoryFactory.makeContentRepository() is called inside the body computed property. SwiftUI may re-evaluate body multiple times, potentially creating new ApolloClient instances and losing cached data.

Consider storing the repository as a property to ensure a single instance:

Proposed fix
 `@main`
 struct ForgeApp: App {
+  private let contentRepository = AppContentRepositoryFactory.makeContentRepository()
+
   var body: some Scene {
     WindowGroup {
-      ForgeRootView(contentRepository: AppContentRepositoryFactory.makeContentRepository())
+      ForgeRootView(contentRepository: contentRepository)
     }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/ios/App/ForgeApp/ForgeApp.swift` around lines 4 - 11, The App
currently calls AppContentRepositoryFactory.makeContentRepository() inside
ForgeApp.body which can be evaluated multiple times and recreate the repository
(and ApolloClient); move the repository creation out of the body into a stored
property on ForgeApp (e.g., a let or lazy var property like contentRepository)
and then pass that property into ForgeRootView(contentRepository:) so the
repository/ApolloClient is created once and reused across body reevaluations.
mobile/ios/Sources/ForgeMobile/Views/ForgeRootView.swift (1)

3-10: Consider making contentRepository and viewModel DEBUG-only to avoid dead storage in Release.

In Release builds, contentRepository and viewModel are stored but never used (since GraphQLTestView is only instantiated in DEBUG). This creates unnecessary memory overhead.

Proposed conditional compilation
 public struct ForgeRootView: View {
+  `#if` DEBUG
   private let contentRepository: ContentRepository?
   `@State` private var viewModel: WatchHomeViewModel?

   public init(contentRepository: ContentRepository? = nil) {
     self.contentRepository = contentRepository
     _viewModel = State(initialValue: contentRepository.map { WatchHomeViewModel(repository: $0) })
   }
+  `#else`
+  public init(contentRepository: ContentRepository? = nil) {}
+  `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/ios/Sources/ForgeMobile/Views/ForgeRootView.swift` around lines 3 -
10, The ForgeRootView currently stores contentRepository and viewModel
unconditionally even though they're only used for GraphQLTestView in DEBUG;
remove dead storage in Release by wrapping the contentRepository property, the
`@State` viewModel, and the initializer logic that initializes WatchHomeViewModel
in `#if` DEBUG / `#endif` blocks (i.e., make contentRepository: ContentRepository?,
`@State` private var viewModel: WatchHomeViewModel?, and the
init(contentRepository:) only exist under DEBUG) and keep the public API and
body for Release unchanged so no DEBUG-only state is allocated when building
Release.
mobile/ios/Sources/ForgeMobile/Data/GraphQLContentClient.swift (1)

63-101: Consider documenting the title fallback priority.

The firstSectionTitle function iterates through sections in order and returns the first non-empty title from media collection, promo banner, info blocks, or CTA. This prioritization is implicit—a brief doc comment explaining the order would aid future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/ios/Sources/ForgeMobile/Data/GraphQLContentClient.swift` around lines
63 - 101, Add a short doc comment above firstSectionTitle describing its
fallback priority and behavior: explain that firstSectionTitle(from:) iterates
sections in order and returns the first non-empty title it finds using the
following precedence: media collection (media.title), promo banner
(promo.promoBannerHeading), info blocks (info.infoBlocksHeading), then CTA
(cta.ctaHeading), and returns nil if none are present; also note that
mapExperienceToContentItem uses this helper to derive the
MobileContentItem.title and falls back to experience.slug when nil.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@mobile/ios/Sources/ForgeMobile/Data/ContentRepository.swift`:
- Around line 22-24: The iOS fetchHome implementation uses slug "experience"
which differs from Android's "home"; update the MobileContentItem retrieval so
fetchHome calls client.getContent(locale:locale, slug: "home") (i.e., change the
slug in the fetchHome function) so both platforms fetch the same content; keep
the function name fetchHome and the client.getContent call unchanged otherwise.

---

Nitpick comments:
In `@mobile/ios/App/ForgeApp/ForgeApp.swift`:
- Around line 4-11: The App currently calls
AppContentRepositoryFactory.makeContentRepository() inside ForgeApp.body which
can be evaluated multiple times and recreate the repository (and ApolloClient);
move the repository creation out of the body into a stored property on ForgeApp
(e.g., a let or lazy var property like contentRepository) and then pass that
property into ForgeRootView(contentRepository:) so the repository/ApolloClient
is created once and reused across body reevaluations.

In `@mobile/ios/Sources/ForgeMobile/Data/GraphQLContentClient.swift`:
- Around line 63-101: Add a short doc comment above firstSectionTitle describing
its fallback priority and behavior: explain that firstSectionTitle(from:)
iterates sections in order and returns the first non-empty title it finds using
the following precedence: media collection (media.title), promo banner
(promo.promoBannerHeading), info blocks (info.infoBlocksHeading), then CTA
(cta.ctaHeading), and returns nil if none are present; also note that
mapExperienceToContentItem uses this helper to derive the
MobileContentItem.title and falls back to experience.slug when nil.

In `@mobile/ios/Sources/ForgeMobile/ViewModels/WatchHomeViewModel.swift`:
- Around line 17-30: In load(locale:) remove the redundant homeError = nil after
a successful fetch: keep the initial homeError = nil at the start of the method,
assign homeItem = item on success and only set homeError in the catch block;
update the success path in the load(locale:) function (which calls
repository.fetchHome) to drop the unnecessary homeError = nil assignment so the
state logic relies on the initial reset and the catch-only error assignment.

In `@mobile/ios/Sources/ForgeMobile/Views/ForgeRootView.swift`:
- Around line 3-10: The ForgeRootView currently stores contentRepository and
viewModel unconditionally even though they're only used for GraphQLTestView in
DEBUG; remove dead storage in Release by wrapping the contentRepository
property, the `@State` viewModel, and the initializer logic that initializes
WatchHomeViewModel in `#if` DEBUG / `#endif` blocks (i.e., make contentRepository:
ContentRepository?, `@State` private var viewModel: WatchHomeViewModel?, and the
init(contentRepository:) only exist under DEBUG) and keep the public API and
body for Release unchanged so no DEBUG-only state is allocated when building
Release.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 06aa284 and f66c004.

📒 Files selected for processing (7)
  • mobile/ios/App/ForgeApp.xcodeproj/project.pbxproj
  • mobile/ios/App/ForgeApp/AppContentRepositoryFactory.swift
  • mobile/ios/App/ForgeApp/ForgeApp.swift
  • mobile/ios/Sources/ForgeMobile/Data/ContentRepository.swift
  • mobile/ios/Sources/ForgeMobile/Data/GraphQLContentClient.swift
  • mobile/ios/Sources/ForgeMobile/ViewModels/WatchHomeViewModel.swift
  • mobile/ios/Sources/ForgeMobile/Views/ForgeRootView.swift

Comment thread mobile/ios/Sources/ForgeMobile/Data/ContentRepository.swift
@Ur-imazing Ur-imazing merged commit 9bbde42 into main Feb 26, 2026
9 of 10 checks passed
@Ur-imazing Ur-imazing deleted the feat/102-graphql branch February 26, 2026 01:35
Kneesal pushed a commit to Kneesal/forge that referenced this pull request Mar 9, 2026
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.

feat(mobile-ios): GraphQL client and codegen for Strapi Experience

1 participant