Skip to content

[CLXR-462][S] Add carrer offline sync#3975

Open
Ahmed-Naguib93 wants to merge 6 commits intofeature/career-offline-modefrom
feature/career-CLXR-462-Offline-Sync
Open

[CLXR-462][S] Add carrer offline sync#3975
Ahmed-Naguib93 wants to merge 6 commits intofeature/career-offline-modefrom
feature/career-CLXR-462-Offline-Sync

Conversation

@Ahmed-Naguib93
Copy link
Copy Markdown
Contributor

feat: add offline sync
refs: CLXR-462
builds: student
affects: student
release note: none

  1. Adds Horizon-specific background offline sync infrastructure to the Canvas Career (Horizon) experience
  2. Introduces HSyncBackgroundTask that iterates logged-in sessions, refreshes course data in the background, and restores the previously active session on completion
  3. Adds HSyncNextDateInteractor and HSyncAccountsInteractor to calculate which sessions are due for sync and when the next background task should fire
  4. Adds HSyncScheduleInteractor to schedule/reschedule the BGProcessingTask after each sync cycle
  5. Stores Horizon sync preferences (frequency, nextDate, autoSyncEnabled, offlineSyncItems) in SessionDefaults per user session
  6. Registers the background task identifier (com.instructure.icanvas.horizon-sync) in Info.plist and StudentAppDelegate
  7. Routes background sync scheduling in applicationDidEnterBackground based on whether the active app is Horizon or Canvas Academic

refs: CLXR-462
builds: student
affects: student
release note: none

test plan: none
refs: CLXR-462
refs:
builds:
affects:
release note:

test plan:
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Review: Horizon Offline Sync Background Task

Overall the implementation is clean and follows existing project patterns well. Good test coverage across the new interactors. A few issues worth addressing:


Issues

  • Failed syncs update the next-sync date as if they succeededHSyncBackgroundTask.syncNextSession (HSyncBackgroundTask.swift ~line 57): the receiveCompletion closure is invoked for both .finished and .failure(_:) without distinguishing between them, so scheduleInteractor.updateNextSyncDate is called even when the network call fails. This pushes the next sync window forward even though no data was actually refreshed.

  • Concrete class subclassing used for mocking instead of protocolsHSyncNextDateInteractor and HSyncScheduleInteractor are public class types that the test suite mocks by subclassing (MockHSyncNextDateInteractor: HSyncNextDateInteractor). This is a testing anti-pattern that prevents the classes from being final and tightly couples the tests to the concrete implementations. Prefer extracting a protocol (e.g. HSyncNextDateInteractorProtocol) and injecting it, which is how the existing CourseSyncInteractor family is structured.

  • Hardcoded uniqueID format in test assertionHSyncBackgroundTaskRequestTests.swift line 43 asserts mock.receivedSessionIDs.sorted() == ["testurl.com-user-1", "testurl2.com-user-2"]. This bakes in the internal LoginSession.uniqueID format (host + "-" + userID). If that format ever changes the test fails for the wrong reason. A more robust check would verify that the IDs passed in match the IDs of the two sessions being used, without hardcoding the derived string.

  • OfflineType.swift copyright year is 2026 — all other new files in this PR use 2025-present. Likely a copy/paste inconsistency.

  • OfflineType has no access modifier — the enum defaults to internal, which may be intentional, but it sits alongside public types in the same directory. Worth confirming this is deliberate and not an accidental omission.


Positive notes

  • The sequential-session sync loop in HSyncBackgroundTask (processing one session at a time via syncNextSession) is a sensible approach for background tasks with limited OS budget.
  • SessionDefaults extensions are well-structured and consistent with existing patterns.
  • HSyncAccountsInteractorTests teardown correctly resets SessionDefaults for each session, avoiding test pollution.
  • Test coverage for HSyncNextDateInteractor is thorough — edge cases (empty list, disabled sync, missing date, empty items) are all exercised.

@bitrise
Copy link
Copy Markdown
Contributor

bitrise Bot commented Apr 4, 2026

Fails
🚫 Build failed, skipping coverage check
Warnings
⚠️ This pull request will not generate a release note.

Affected Apps: Student

Builds: Student

❌ Swift lint
----- xcbeautify -----
Version: 2.30.1
----------------------

❌ /Users/vagrant/git/Horizon/Horizon/Sources/Features/OfflineSync/OfflineType.swift:24:1: Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)

Generated by 🚫 dangerJS against b66004f

@bitrise
Copy link
Copy Markdown
Contributor

bitrise Bot commented Apr 4, 2026

Builds

Commit: feat: add more test cases (ba12e8c)
Build Number: 1730
Built At: Apr 04 20:56 CEST (04/04 12:56 PM MDT)

Student

@bitrise
Copy link
Copy Markdown
Contributor

bitrise Bot commented Apr 4, 2026

Warnings
⚠️ This pull request will not generate a release note.
⚠️ One or more files are below the minimum test coverage 50%

Affected Apps: Student

Builds: Student

Coverage New % Master % Delta
Canvas iOS 91.89% 81.06% 10.82%
Horizon/Horizon/Sources/Features/OfflineSync/OfflineType.swift 0% -- --
Horizon/Horizon/Sources/Features/OfflineSync/BackgroundTask/HSyncScheduleInteractor.swift 46.67% -- --

Generated by 🚫 dangerJS against ba12e8c

import Core

public final class HSyncBackgroundTaskRequest: BGProcessingTaskRequest {
public static let ID = "com.instructure.icanvas.horizon-sync"
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.

let's call this com.instructure.icanvas.career-sync

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).offline-sync</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).horizon-sync</string>
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.

let's call this career-offline-sync

Comment on lines +192 to +196
if AppEnvironment.shared.app == .horizon {
HSyncScheduleInteractorLive().scheduleNextSync()
} else {
OfflineSyncScheduleInteractor().scheduleNextSync()
}
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.

  • With this if-else condition based on the currently logged in user, either academic or horizon accounts will sync but if we have mixed academic/horizon users one of them won't sync.
  • If we trigger two background syncs, the OS will throttle down and one of them won't run because the background wake-up frequency would be too high.
  • The academic sync logic already iterates through all accounts and syncs each one where this feature is turned on. I think the best approach would be to share the offline sync background task between the two experiences and execute the proper sync logic based on the user's experience.

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

Labels

career Canvas Career Experience

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants