Skip to content

Commit

Permalink
Home banners (#192)
Browse files Browse the repository at this point in the history
  • Loading branch information
EvanCooper9 authored Nov 18, 2023
1 parent 6f86f6c commit 3b052d7
Show file tree
Hide file tree
Showing 28 changed files with 322 additions and 118 deletions.
10 changes: 7 additions & 3 deletions Friendly Competitions Tests/Models/Deep lInk/DeepLinkTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ final class DeepLinkTests: FCTestCase {
func testThatCompetitionResultsCanBeInitialized() {
let url = URL(string: "https://friendly-competitions.app/competition/abc123/results")!
let deepLink = DeepLink(from: url)
XCTAssertEqual(deepLink, .competitionResults(id: "abc123"))
XCTAssertEqual(deepLink, .competitionResult(id: "abc123", resultID: nil))
}

func testThatUrlIsCorrect() {
Expand All @@ -31,8 +31,12 @@ final class DeepLinkTests: FCTestCase {
URL(string: "https://friendly-competitions.app/competition/\(#function)")!
)
XCTAssertEqual(
DeepLink.competitionResults(id: #function).url,
URL(string: "https://friendly-competitions.app/competition/\(#function)/results")!
DeepLink.competitionResult(id: #function, resultID: nil).url,
URL(string: "https://friendly-competitions.app/competition/\(#function)")!
)
XCTAssertEqual(
DeepLink.competitionResult(id: "abc", resultID: "123").url,
URL(string: "https://friendly-competitions.app/competition/abc/results/123")!
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ final class CompetitionViewModelTests: FCTestCase {
competitionsManager.competitionPublisherForReturnValue = .never()
competitionsManager.resultsForReturnValue = .never()
competitionsManager.standingsPublisherForReturnValue = .never()
competitionsManager.unseenResults = .never()
healthKitManager.shouldRequestReturnValue = .never()
notificationsManager.requestPermissionsReturnValue = .never()
searchManager.searchForUsersWithIDsReturnValue = .never()
userManager.user = .evan
userManager.userPublisher = .just(.evan)
Expand Down
20 changes: 17 additions & 3 deletions Friendly Competitions Tests/Views/Home/HomeViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,32 @@ final class HomeViewModelTests: FCTestCase {

let competition = Competition.mock
competitionsManager.searchByIDReturnValue = .just(competition)
let competitionResult = CompetitionResult(id: "abc", start: .distantPast, end: .now, participants: [])

let competitionDocument = DocumentMock<Competition>()
competitionDocument.getClosure = { _, _ in .just(competition) }
let competitionResultDocument = DocumentMock<CompetitionResult>()
competitionResultDocument.getClosure = { _, _ in .just(competitionResult) }
database.documentClosure = { path -> Document in
path.contains("result") ? competitionResultDocument : competitionDocument
}

let user = User.evan
friendsManager.userWithIdReturnValue = .just(user)

let viewModel = HomeViewModel()

deepLinkSubject.send(.competition(id: competition.id))
scheduler.advance()
XCTAssertEqual(viewModel.deepLinkedNavigationDestination, .competition(competition))
XCTAssertEqual(viewModel.deepLinkedNavigationDestination, .competition(competition, nil))

deepLinkSubject.send(.competitionResult(id: competition.id, resultID: nil))
scheduler.advance()
XCTAssertEqual(viewModel.deepLinkedNavigationDestination, .competition(competition, nil))

deepLinkSubject.send(.competitionResults(id: competition.id))
deepLinkSubject.send(.competitionResult(id: competition.id, resultID: "abc"))
scheduler.advance()
XCTAssertEqual(viewModel.deepLinkedNavigationDestination, .competition(competition))
XCTAssertEqual(viewModel.deepLinkedNavigationDestination, .competition(competition, competitionResult))

deepLinkSubject.send(.user(id: user.id))
scheduler.advance()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import XCTest
final class NavigationDestinationTests: FCTestCase {
func testThatIDIsCorrect() {
let competition = Competition.mock
XCTAssertEqual(NavigationDestination.competition(competition).id, competition.id)
XCTAssertEqual(NavigationDestination.competition(competition, nil).id, competition.id)

let user = User.evan
XCTAssertEqual(NavigationDestination.user(user).id, user.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@
"version" : "8.18.2"
}
},
{
"identity" : "app-check",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/app-check.git",
"state" : {
"revision" : "5746b2d35c91c50581590ed97abe4c06b5037274",
"version" : "10.18.0"
}
},
{
"identity" : "combine-schedulers",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -68,17 +77,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "837d4af6ead57cec1fc38007892500d3139c7556",
"version" : "10.16.0"
"revision" : "5de0369ee79ad096c164eb3afeb7921d92a43b58",
"version" : "10.18.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "56f681586ff006a7982b53dc94082eea31971acf",
"version" : "10.16.0"
"revision" : "6b332152355c372ace9966d8ee76ed191f97025e",
"version" : "10.17.0"
}
},
{
Expand All @@ -95,8 +104,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "c38ce365d77b04a9a300c31061c5227589e5597b",
"version" : "7.11.5"
"revision" : "bc27fad73504f3d4af235de451f02ee22586ebd3",
"version" : "7.12.1"
}
},
{
Expand Down Expand Up @@ -140,8 +149,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b",
"version" : "1.22.2"
"revision" : "9d108e9112aa1d65ce508facf804674546116d9c",
"version" : "1.22.3"
}
},
{
Expand Down Expand Up @@ -176,8 +185,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/RevenueCat/purchases-ios",
"state" : {
"revision" : "d50a261eeb8a635c5927670f223971e13a34a926",
"version" : "4.28.1"
"revision" : "dcb1bc08be560b626207a5782eca6ed132149409",
"version" : "4.30.4"
}
},
{
Expand All @@ -194,8 +203,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "ea631ce892687f5432a833312292b80db238186a",
"version" : "1.0.0"
"revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71",
"version" : "1.1.0"
}
},
{
Expand All @@ -221,8 +230,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "3c54ab05249f59f2c6641dd2920b8358ea9ed127",
"version" : "1.24.0"
"revision" : "07f7f26ded8df9645c072f220378879c4642e063",
"version" : "1.25.1"
}
},
{
Expand All @@ -231,7 +240,7 @@
"location" : "https://github.com/SwiftUIX/SwiftUIX",
"state" : {
"branch" : "master",
"revision" : "cf729fcab44196ed7361293bcad493a0e928fb24"
"revision" : "5a36d1d6d6b31fd5febd38fb0c3845035da3b871"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ protocol CompetitionsManaging {
var invitedCompetitions: AnyPublisher<[Competition], Never> { get }
var appOwnedCompetitions: AnyPublisher<[Competition], Never> { get }
var hasPremiumResults: AnyPublisher<Bool, Never> { get }
var unseenResults: AnyPublisher<[(Competition, CompetitionResult.ID)], Never> { get }

func create(_ competition: Competition) -> AnyPublisher<Void, Error>
func update(_ competition: Competition) -> AnyPublisher<Void, Error>
func search(byID competitionID: Competition.ID) -> AnyPublisher<Competition, Error>
func results(for competitionID: Competition.ID) -> AnyPublisher<[CompetitionResult], Error>
func standings(for competitionID: Competition.ID, resultID: CompetitionResult.ID) -> AnyPublisher<[Competition.Standing], Error>

func viewedResults(competitionID: Competition.ID, resultID: CompetitionResult.ID)
func competitionPublisher(for competitionID: Competition.ID) -> AnyPublisher<Competition, Error>
func standingsPublisher(for competitionID: Competition.ID) -> AnyPublisher<[Competition.Standing], Error>
}
Expand Down Expand Up @@ -50,6 +51,41 @@ final class CompetitionsManager: CompetitionsManaging {
}
.removeDuplicates { $0.id == $1.id }
.flatMapLatest(withUnretained: self) { $0.hasPremiumResults(for: $1.competitions, id: $1.id) }
.share(replay: 1)
.eraseToAnyPublisher()
}()

private(set) lazy var unseenResults: AnyPublisher<[(Competition, CompetitionResult.ID)], Never> = {
guard featureFlagManager.value(forBool: .newResultsBannerEnabled) else {
return .just([])
}

let competitions = competitions.removeDuplicates { $0.map(\.id) == $1.map(\.id) }

return Publishers
.CombineLatest(competitions, $seenResultsIDs)
.flatMapLatest(withUnretained: self) { strongSelf, result in
let (competitions, seenResultsIDs) = result
return competitions
.map { competition in
strongSelf.database.collection("competitions/\(competition.id)/results")
.sorted(by: "end", direction: .descending)
.limit(1)
.getDocuments(ofType: CompetitionResult.self)
.map { results -> (Competition, CompetitionResult.ID)? in
guard let result = results.first, let seenResultsIDs else { return nil }
let id = [competition.id, result.id].joined(separator: "-")
guard !seenResultsIDs.contains(id) else { return nil }
return (competition, result.id)
}
.catchErrorJustReturn(nil)
.eraseToAnyPublisher()
}
.combineLatest()
.compactMapMany { $0 }
.eraseToAnyPublisher()
}
.share(replay: 1)
.eraseToAnyPublisher()
}()

Expand All @@ -60,16 +96,19 @@ final class CompetitionsManager: CompetitionsManaging {
private let appOwnedCompetitionsSubject = ReplaySubject<[Competition], Never>(bufferSize: 1)
private let hasPremiumResultsSubject = ReplaySubject<Bool, Never>(bufferSize: 1)

@Injected(\.api) private var api
@Injected(\.appState) private var appState
@Injected(\.analyticsManager) private var analyticsManager
@Injected(\.competitionCache) private var cache
@Injected(\.database) private var database
@Injected(\.environmentManager) private var environmentManager
@Injected(\.userManager) private var userManager
@Injected(\.api) private var api: API
@Injected(\.appState) private var appState: AppStateProviding
@Injected(\.analyticsManager) private var analyticsManager: AnalyticsManaging
@Injected(\.competitionCache) private var cache: CompetitionCache
@Injected(\.database) private var database: Database
@Injected(\.environmentManager) private var environmentManager: EnvironmentManaging
@Injected(\.featureFlagManager) private var featureFlagManager: FeatureFlagManaging
@Injected(\.userManager) private var userManager: UserManaging

private var cancellables = Cancellables()

@UserDefault("seenResultsIDs") private var seenResultsIDs: [String]?

// MARK: - Lifecycle

init() {
Expand All @@ -78,6 +117,8 @@ final class CompetitionsManager: CompetitionsManaging {
appOwnedCompetitions = appOwnedCompetitionsSubject.eraseToAnyPublisher()

listenForCompetitions()

seenResultsIDs = []
}

// MARK: - Public Methods
Expand Down Expand Up @@ -146,6 +187,11 @@ final class CompetitionsManager: CompetitionsManaging {
.eraseToAnyPublisher()
}

func viewedResults(competitionID: Competition.ID, resultID: CompetitionResult.ID) {
let id = [competitionID, resultID].joined(separator: "-")
seenResultsIDs = (seenResultsIDs ?? []).appending(id)
}

// MARK: - Private Methods

private func listenForCompetitions() {
Expand Down
14 changes: 1 addition & 13 deletions Friendly Competitions/Managers/Feature Flag/FeatureFlag.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
protocol FeatureFlag {
associatedtype Data
var stringValue: String { get }
var defaultValue: Data { get }
}

enum FeatureFlagDouble: String, FeatureFlag {
Expand All @@ -10,24 +9,13 @@ enum FeatureFlagDouble: String, FeatureFlag {
case databaseCacheTtl = "database_cache_ttl"

var stringValue: String { rawValue }
var defaultValue: Data {
switch self {
case .databaseCacheTtl:
return 5.days
}
}
}

enum FeatureFlagBool: String, FeatureFlag {
typealias Data = Bool

case premiumEnabled = "premium_enabled"
case newResultsBannerEnabled = "new_results_banner_enabled"

var stringValue: String { rawValue }
var defaultValue: Bool {
switch self {
case .premiumEnabled:
return false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import Factory

extension Container {
var featureFlagManager: Factory<FeatureFlagManaging> {
self { FeatureFlagManager() }
self { FeatureFlagManager() }.scope(.shared)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Foundation
import FirebaseRemoteConfig
import FirebaseRemoteConfigSwift

// sourcery: AutoMockable
protocol FeatureFlagManaging {
Expand All @@ -18,8 +17,7 @@ final class FeatureFlagManager: FeatureFlagManaging {
error.reportToCrashlytics()
} else if status == .success {
self.remoteConfig.activate { _, error in
guard let error else { return }
error.reportToCrashlytics()
error?.reportToCrashlytics()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

struct CompetitionResult: Codable, Identifiable {
struct CompetitionResult: Codable, Hashable, Identifiable {
let id: String
@PostDecoded<DateToStartOfDay, Date> var start: Date
@PostDecoded<DateToEndOfDay, Date> var end: Date
Expand Down
Loading

0 comments on commit 3b052d7

Please sign in to comment.