Skip to content

Commit

Permalink
Refactor settings, profile picture and more (#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
EvanCooper9 authored Sep 13, 2024
1 parent 185c05b commit 92306e0
Show file tree
Hide file tree
Showing 47 changed files with 973 additions and 681 deletions.
30 changes: 0 additions & 30 deletions FCKit/Sources/FCKit/FeatureFlag/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,3 @@ public protocol FeatureFlag: CaseIterable, RawRepresentable {
extension FeatureFlag where RawValue == String {
public var stringValue: String { rawValue }
}

public enum FeatureFlagBool: String, CaseIterable, FeatureFlag {
public typealias Data = Bool

case adsEnabled = "ads_enabled"
case newResultsBannerEnabled = "new_results_banner_enabled"
case ignoreManuallyEnteredHealthKitData = "ignore_manually_entered_health_kit_data"

public var defaultValue: Data { false }
}

public enum FeatureFlagDouble: String, FeatureFlag {
public typealias Data = Double

case databaseCacheTtl = "database_cache_ttl"
case healthKitBackgroundDeliveryTimeoutMS = "health_kit_background_delivery_timeout_ms"
case widgetUpdateIntervalS = "widget_update_interval_s"
case dataUploadGracePeriodHours = "data_upload_grace_period_hours"

public var defaultValue: Data { 0 }
}

public enum FeatureFlagString: String, FeatureFlag {
public typealias Data = String

case googleAdsHomeScreenAdUnit = "google_ads_home_screen_ad_unit"
case googleAdsExploreScreenAdUnit = "google_ads_explore_screen_ad_unit"

public var defaultValue: Data { "" }
}
9 changes: 9 additions & 0 deletions FCKit/Sources/FCKit/FeatureFlag/Flags/FeatureFlagBool.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
public enum FeatureFlagBool: String, CaseIterable, FeatureFlag {
public typealias Data = Bool

case adsEnabled = "ads_enabled"
case newResultsBannerEnabled = "new_results_banner_enabled"
case ignoreManuallyEnteredHealthKitData = "ignore_manually_entered_health_kit_data"

public var defaultValue: Data { false }
}
10 changes: 10 additions & 0 deletions FCKit/Sources/FCKit/FeatureFlag/Flags/FeatureFlagDouble.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
public enum FeatureFlagDouble: String, FeatureFlag {
public typealias Data = Double

case databaseCacheTtl = "database_cache_ttl"
case healthKitBackgroundDeliveryTimeoutMS = "health_kit_background_delivery_timeout_ms"
case widgetUpdateIntervalS = "widget_update_interval_s"
case dataUploadGracePeriodHours = "data_upload_grace_period_hours"

public var defaultValue: Data { 0 }
}
17 changes: 17 additions & 0 deletions FCKit/Sources/FCKit/FeatureFlag/Flags/FeatureFlagString.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Foundation

public enum FeatureFlagString: String, FeatureFlag {
public typealias Data = String

case googleAdsHomeScreenAdUnit = "google_ads_home_screen_ad_unit"
case googleAdsExploreScreenAdUnit = "google_ads_explore_screen_ad_unit"
case minimumAppVersion = "ios_minimum_app_version"

public var defaultValue: Data {
switch self {
case .googleAdsHomeScreenAdUnit: return ""
case .googleAdsExploreScreenAdUnit: return ""
case .minimumAppVersion: return Bundle.main.version
}
}
}
150 changes: 71 additions & 79 deletions FriendlyCompetitions.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@
"version" : "11.0.1"
}
},
{
"identity" : "brightroom",
"kind" : "remoteSourceControl",
"location" : "https://github.com/FluidGroup/Brightroom",
"state" : {
"revision" : "b9f1f9b8144f0bd2d94d64f77f033fcf72c00260",
"version" : "2.10.1"
}
},
{
"identity" : "combine-schedulers",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -72,15 +81,6 @@
"version" : "2.3.2"
}
},
{
"identity" : "files",
"kind" : "remoteSourceControl",
"location" : "https://github.com/JohnSundell/Files",
"state" : {
"revision" : "d273b5b7025d386feef79ef6bad7de762e106eaf",
"version" : "4.2.0"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -171,6 +171,15 @@
"version" : "2.4.0"
}
},
{
"identity" : "rxswift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/ReactiveX/RxSwift.git",
"state" : {
"revision" : "b06a8c8596e4c3e8e7788e08e720e3248563ce6a",
"version" : "6.7.1"
}
},
{
"identity" : "swift-algorithms",
"kind" : "remoteSourceControl",
Expand All @@ -180,6 +189,24 @@
"version" : "1.2.0"
}
},
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "cd142fd2f64be2100422d658e7411e39489da985",
"version" : "1.2.0"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/FluidGroup/swift-collections",
"state" : {
"revision" : "939cfd25234472b4dc91c3caeab304d15bca9a73",
"version" : "1.1.0"
}
},
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
Expand All @@ -189,6 +216,15 @@
"version" : "1.1.0"
}
},
{
"identity" : "swift-concurrency-task-manager",
"kind" : "remoteSourceControl",
"location" : "https://github.com/VergeGroup/swift-concurrency-task-manager",
"state" : {
"revision" : "340cf14e0282977deeeb436605d1810ce4f4fbc9",
"version" : "1.4.0"
}
},
{
"identity" : "swift-numerics",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -234,6 +270,24 @@
"revision" : "7000c5cccbe5cdde0a2d24dbe61b305290b73acb"
}
},
{
"identity" : "transitionpatch",
"kind" : "remoteSourceControl",
"location" : "https://github.com/FluidGroup/TransitionPatch.git",
"state" : {
"revision" : "88aac5305fe568ef62f73a3cf19f0b8392ffb52c",
"version" : "1.0.5"
}
},
{
"identity" : "verge",
"kind" : "remoteSourceControl",
"location" : "https://github.com/VergeGroup/Verge.git",
"state" : {
"revision" : "23b6e92fbf88e72ce0546144fbc1dc455cb5d53d",
"version" : "11.5.5"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
Expand Down
1 change: 1 addition & 0 deletions FriendlyCompetitions/AppServices/AppServices+Factory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ extension Container {
NotificationsAppService()
BackgroundJobsAppService()
GoogleAdsAppService()
StorageAppService()
}
}
.scope(.singleton)
Expand Down
17 changes: 17 additions & 0 deletions FriendlyCompetitions/AppServices/Storage/StorageAppService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import ECKit
import Factory

final class StorageAppService: AppService {

// MARK: - AppService

func didFinishLaunching() {
Task { [storageManager] in
storageManager.clear(ttl: 60.days)
}
}

// MARK: - Private

@LazyInjected(\.storageManager) private var storageManager: StorageManaging
}
17 changes: 15 additions & 2 deletions FriendlyCompetitions/FriendlyCompetitionsAppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,25 @@ final class FriendlyCompetitionsAppModel: ObservableObject {

// MARK: - Public Properties

@Published private(set)var loggedIn = false
@Published private(set)var emailVerified = false
@Published private(set) var needsUpgrade = false
@Published private(set) var loggedIn = false
@Published private(set) var emailVerified = false
@Published var hud: HUD?

// MARK: - Private Properties

@Injected(\.analyticsManager) private var analyticsManager: AnalyticsManaging
@Injected(\.appState) private var appState: AppStateProviding
@Injected(\.authenticationManager) private var authenticationManager: AuthenticationManaging
@Injected(\.featureFlagManager) private var featureFlagManager: FeatureFlagManaging

// MARK: - Lifecycle

init() {
authenticationManager.loggedIn.assign(to: &$loggedIn)
authenticationManager.emailVerified.assign(to: &$emailVerified)
appState.hud.assign(to: &$hud)
checkMinimumVersion()
}

// MARK: - Public Methods
Expand All @@ -36,4 +39,14 @@ final class FriendlyCompetitionsAppModel: ObservableObject {
func opened(url: URL) {
analyticsManager.log(event: .urlOpened(url: url))
}

// MARK: - Private Methods

private func checkMinimumVersion() {
#if RELEASE
let currentVersion = Bundle.main.version
let minimumVersion = featureFlagManager.value(forString: .minimumAppVersion)
needsUpgrade = currentVersion.compare(minimumVersion, options: .numeric) == .orderedAscending
#endif
}
}
122 changes: 109 additions & 13 deletions FriendlyCompetitions/Managers/Storage/StorageManager.swift
Original file line number Diff line number Diff line change
@@ -1,34 +1,130 @@
import Combine
import ECKit
import Factory
import Files
import FirebaseStorage
import FirebaseStorageCombineSwift
import Foundation

// sourcery: AutoMockable
protocol StorageManaging {
func data(for storagePath: String) -> AnyPublisher<Data, Error>
func get(_ path: String) -> AnyPublisher<Data, Error>
func set(_ path: String, data: Data?) -> AnyPublisher<Void, Error>
func clear(ttl: TimeInterval)
}

extension StorageManaging {
func clear(ttl: TimeInterval = 0) {
clear(ttl: ttl)
}
}

final class StorageManager: StorageManaging {

enum StorageManagerError: Error {
case unknown
case fileNotFound
}

private struct FileMetadata: Codable {
let accessed: Date
let updated: Date
}

// MARK: - Private Properties

@Injected(\.storage) private var storage
private let fileManager = FileManager.default
private let storage = Storage.storage()

@UserDefault("file-metadata", defaultValue: [:]) private var fileMetadata: [String: FileMetadata]

init() {
let environment = Container.shared.environmentManager.resolve().environment
switch environment {
case .prod:
break
case .debugLocal:
storage.useEmulator(withHost: "localhost", port: 9199)
case .debugRemote(let destination):
storage.useEmulator(withHost: destination, port: 9199)
}
}

// MARK: - Public Methods

func data(for storagePath: String) -> AnyPublisher<Data, Error> {
guard let documents = Folder.documents?.url else {
return storage.data(path: storagePath)
func get(_ path: String) -> AnyPublisher<Data, any Error> {
let url = URL.cachesDirectory.appending(path: path)
let cachedData = fileManager.contents(atPath: url.path(percentEncoded: false))

return .fromAsync { [weak self, fileManager, storage] in
guard let self else { throw StorageManagerError.unknown }

let reference = storage.reference(withPath: path)
let serverMetadata = try await reference.getMetadata()
let serverMetadataDate = serverMetadata.updated ?? serverMetadata.timeCreated ?? .now
defer { fileMetadata[path] = FileMetadata(accessed: .now, updated: serverMetadataDate) }
let url = URL.cachesDirectory.appending(path: path)

if let fileMetadata = fileMetadata[path],
serverMetadataDate <= fileMetadata.updated,
fileManager.fileExists(atPath: url.path(percentEncoded: false)),
let contents = fileManager.contents(atPath: url.path(percentEncoded: false)) {
return contents
} else {
let url = try await reference.writeAsync(toFile: url)
guard let contents = fileManager.contents(atPath: url.path(percentEncoded: false)) else {
throw StorageManagerError.fileNotFound
}
return contents
}
}
.prepend([cachedData].compacted())
.eraseToAnyPublisher()
}

let localPath = documents.appendingPathComponent(storagePath)
let localData = try? Data(contentsOf: localPath)
if let localData, !localData.isEmpty {
return .just(localData)
func set(_ path: String, data: Data?) -> AnyPublisher<Void, any Error> {
Future { [fileManager, storage] promise in
let reference = storage.reference(withPath: path)
if let data {
reference.putData(data, metadata: nil) { result in
switch result {
case .success(let success):
let url = URL.cachesDirectory.appending(path: path)
fileManager.createFile(atPath: url.path, contents: data)
promise(.success(()))
case .failure(let error):
promise(.failure(error))
}
}
} else {
reference.delete { error in
if let error {
promise(.failure(error))
} else {
let url = URL.cachesDirectory.appending(path: path)
try? fileManager.removeItem(at: url)
promise(.success(()))
}
}
}
}
.eraseToAnyPublisher()
}

func clear(ttl: TimeInterval) {
let paths = fileMetadata
.filter { _, metadata in metadata.accessed.addingTimeInterval(ttl) < .now }
.keys

return storage.data(path: storagePath)
.handleEvents(receiveOutput: { _ = try? Folder.documents?.createFileIfNeeded(at: storagePath, contents: $0) })
.eraseToAnyPublisher()
paths.forEach { path in
fileMetadata.removeValue(forKey: path)
let url = URL.cachesDirectory.appending(path: path)
try? fileManager.removeItem(at: url)
}
}
}

private extension Int64 {
var bytes: Int64 { self }
var kb: Int64 { bytes * 1024 }
var mb: Int64 { kb * 1024 }
}
Loading

0 comments on commit 92306e0

Please sign in to comment.