├── .github
└── workflows
│ ├── deploy-firebase.yml
│ ├── deploy-ios.yml
│ └── test-ios.yml
├── .gitignore
├── .ruby-version
├── .sourcery.yml
├── .swiftlint.yml
├── .vscode
└── settings.json
├── Brewfile
├── Brewfile.lock.json
├── CONTRIBUTING.md
├── FCKit
├── .gitignore
├── .sourcery.yml
├── .swiftpm
│ └── xcode
│ │ ├── package.xcworkspace
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcshareddata
│ │ └── xcschemes
│ │ └── FCKit.xcscheme
├── Package.swift
├── Sources
│ ├── FCKit
│ │ ├── Analytics
│ │ │ ├── AnalyticsEvent.swift
│ │ │ ├── AnalyticsManager+Factory.swift
│ │ │ └── AnalyticsManager.swift
│ │ ├── Database
│ │ │ ├── Database+Factory.swift
│ │ │ ├── Database+Firebase.swift
│ │ │ ├── Database.swift
│ │ │ └── Settings
│ │ │ │ ├── DatabaseSettingsManager.swift
│ │ │ │ └── DatabaseSettingsStore.swift
│ │ ├── Environment
│ │ │ ├── EnvironmentCache.swift
│ │ │ ├── EnvironmentManager+Factory.swift
│ │ │ ├── EnvironmentManager.swift
│ │ │ └── FCEnvironment.swift
│ │ ├── Extensions
│ │ │ ├── Combine
│ │ │ │ └── Publisher+Extensions.swift
│ │ │ ├── Foundation
│ │ │ │ └── UserDefaults+Extensions.swift
│ │ │ └── StdLib
│ │ │ │ └── Error+Extensions.swift
│ │ ├── FeatureFlag
│ │ │ ├── FeatureFlag.swift
│ │ │ ├── FeatureFlagManager+Factory.swift
│ │ │ ├── FeatureFlagManager.swift
│ │ │ └── Flags
│ │ │ │ ├── FeatureFlagBool.swift
│ │ │ │ ├── FeatureFlagDouble.swift
│ │ │ │ └── FeatureFlagString.swift
│ │ ├── Firebase
│ │ │ └── FirebaseConfigureHelper.swift
│ │ └── Util
│ │ │ ├── AppGroup.swift
│ │ │ ├── PostDecoded
│ │ │ ├── DateToEndOfDay.swift
│ │ │ ├── DateToStartOfDay.swift
│ │ │ └── PostDecoded.swift
│ │ │ └── WidgetIdentifier.swift
│ └── FCKitMocks
│ │ ├── AutoMockable.generated.swift
│ │ └── Database+Mock.swift
├── Tests
│ └── FCKitTests
│ │ └── Environment
│ │ └── EnvironmentManagerTests.swift
└── sourcery
│ └── templates
│ └── AutoMockable.stencil
├── FriendlyCompetitions.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ ├── WorkspaceSettings.xcsettings
│ │ └── swiftpm
│ │ └── Package.resolved
└── xcshareddata
│ └── xcschemes
│ ├── FriendlyCompetitions.xcscheme
│ └── FriendlyCompetitionsWidgets.xcscheme
├── FriendlyCompetitions
├── API
│ ├── API+Factory.swift
│ ├── API+Firebase.swift
│ ├── API.swift
│ └── Endpoint.swift
├── AppDelegate.swift
├── AppLauncher.swift
├── AppServices
│ ├── AppService.swift
│ ├── AppServices+Factory.swift
│ ├── BackgroundJobs
│ │ ├── BackgroundJob.swift
│ │ ├── BackgroundJobsAppService.swift
│ │ └── Jobs
│ │ │ ├── FetchCompetitionResultsBackgroundJob.swift
│ │ │ └── FetchDocumentBackgroundJob.swift
│ ├── DataUploading
│ │ └── DataUploadingAppService.swift
│ ├── Developer
│ │ └── DeveloperAppService.swift
│ ├── Firebase
│ │ ├── FCAppCheckProviderFactory.swift
│ │ └── FirebaseAppService.swift
│ ├── GoogleAds
│ │ └── GoogleAdsAppService.swift
│ ├── Notifications
│ │ └── NotificationsAppService.swift
│ └── Storage
│ │ └── StorageAppService.swift
├── AppState
│ ├── AppState+Factory.swift
│ └── AppState.swift
├── FriendlyCompetitions.entitlements
├── FriendlyCompetitions.swift
├── FriendlyCompetitionsAppModel.swift
├── FriendlyCompetitionsTests.swift
├── Info.plist
├── IssueReporting
│ ├── IssueReporting+Factory.swift
│ └── IssueReporting.swift
├── Managers
│ ├── ActivitySummary
│ │ ├── ActivitySummaryCache.swift
│ │ ├── ActivitySummaryManager+Factory.swift
│ │ └── ActivitySummaryManager.swift
│ ├── Authentication
│ │ ├── AuthProviding
│ │ │ ├── AuthProviding+Firebase.swift
│ │ │ └── AuthProviding.swift
│ │ ├── AuthenticationCache.swift
│ │ ├── AuthenticationError.swift
│ │ ├── AuthenticationManager+Factory.swift
│ │ ├── AuthenticationManager.swift
│ │ ├── AuthenticationMethod.swift
│ │ └── SignInWithAppleProvider.swift
│ ├── BackgroundRefresh
│ │ ├── BackgroundRefreshManager+Factory.swift
│ │ ├── BackgroundRefreshManager.swift
│ │ └── BackgroundRefreshStatus.swift
│ ├── Banner
│ │ ├── BannerManager+Factory.swift
│ │ └── BannerManager.swift
│ ├── Competitions
│ │ ├── CompetitionsManager+Factory.swift
│ │ └── CompetitionsManager.swift
│ ├── Friends
│ │ ├── FriendsManager+Factory.swift
│ │ └── FriendsManager.swift
│ ├── HealthKit
│ │ ├── HealthKitBackgroundDeliveryError.swift
│ │ ├── HealthKitManager+Factory.swift
│ │ ├── HealthKitManager.swift
│ │ ├── HealthKitPermissionType.swift
│ │ ├── HealthKitQuery.swift
│ │ └── HealthStoring.swift
│ ├── Notifications
│ │ ├── NotificationsManager+Factory.swift
│ │ └── NotificationsManager.swift
│ ├── Search
│ │ ├── SearchClient.swift
│ │ ├── SearchManager+Factory.swift
│ │ └── SearchManager.swift
│ ├── StepCount
│ │ ├── StepCount.swift
│ │ ├── StepCountManager+Factory.swift
│ │ └── StepCountManager.swift
│ ├── Storage
│ │ ├── StorageManager+Factory.swift
│ │ └── StorageManager.swift
│ ├── User
│ │ ├── UserManager+Factory.swift
│ │ └── UserManager.swift
│ └── Workout
│ │ ├── WorkoutManager+Factory.swift
│ │ └── WorkoutManager.swift
├── Models
│ ├── ActivitySummary
│ │ └── ActivitySummary.swift
│ ├── Competition
│ │ ├── Competition+ScoringModel.swift
│ │ ├── Competition+Standing.swift
│ │ ├── Competition.swift
│ │ └── CompetitionResult.swift
│ ├── DeepLink
│ │ └── DeepLink.swift
│ ├── Permission
│ │ ├── Permission.swift
│ │ └── PermissionStatus.swift
│ ├── Stored.swift
│ ├── User
│ │ ├── User+Statistics.swift
│ │ ├── User+Tag.swift
│ │ ├── User+Visibility.swift
│ │ └── User.swift
│ └── Workouts
│ │ ├── Workout.swift
│ │ ├── WorkoutMetric.swift
│ │ └── WorkoutType.swift
├── PreviewContent
│ ├── Helpers
│ │ └── PreviewHelper.swift
│ ├── Mocks
│ │ └── Models
│ │ │ ├── ActivitySummary+Mock.swift
│ │ │ ├── Competition+Mock.swift
│ │ │ ├── User+Mock.swift
│ │ │ └── Workout+Mock.swift
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── PrivacyInfo.xcprivacy
├── Resources
│ ├── Assets
│ │ ├── Colors.xcassets
│ │ │ ├── Branded
│ │ │ │ ├── Blue.colorset
│ │ │ │ │ └── Contents.json
│ │ │ │ ├── Contents.json
│ │ │ │ ├── Green.colorset
│ │ │ │ │ └── Contents.json
│ │ │ │ └── Red.colorset
│ │ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ └── Images.xcassets
│ │ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── Icon-App-20x20@1x.png
│ │ │ ├── Icon-App-20x20@2x.png
│ │ │ ├── Icon-App-20x20@3x.png
│ │ │ ├── Icon-App-29x29@1x.png
│ │ │ ├── Icon-App-29x29@2x.png
│ │ │ ├── Icon-App-29x29@3x.png
│ │ │ ├── Icon-App-40x40@1x.png
│ │ │ ├── Icon-App-40x40@2x.png
│ │ │ ├── Icon-App-40x40@3x.png
│ │ │ ├── Icon-App-60x60@2x.png
│ │ │ ├── Icon-App-60x60@3x.png
│ │ │ ├── Icon-App-76x76@1x.png
│ │ │ ├── Icon-App-76x76@2x.png
│ │ │ ├── Icon-App-83.5x83.5@2x.png
│ │ │ └── ItunesArtwork@2x.png
│ │ │ ├── Contents.json
│ │ │ ├── Permissions
│ │ │ ├── Contents.json
│ │ │ ├── health.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── health.png
│ │ │ └── notifications.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── notifications.png
│ │ │ ├── Privacy
│ │ │ ├── Contents.json
│ │ │ ├── nameHidden.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ ├── nameHiddenDark.png
│ │ │ │ └── nameHiddenLight.png
│ │ │ └── nameShown.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ ├── nameShownDark.png
│ │ │ │ └── nameShownLight.png
│ │ │ └── logo.imageset
│ │ │ ├── Contents.json
│ │ │ └── raw.png
│ └── Strings
│ │ ├── en.lproj
│ │ └── Localizable.strings
│ │ └── fr-CA.lproj
│ │ └── Localizable.strings
├── Sourcery
│ └── AutoMockable.generated.swift
├── SwiftGen
│ ├── Strings+Generated.swift
│ └── XCAssets+Generated.swift
├── Utility
│ ├── AppInfo.swift
│ ├── ArrayBuilder.swift
│ ├── Device.swift
│ ├── Extensions
│ │ ├── Foundation
│ │ │ ├── Array+Extensions.swift
│ │ │ ├── DateInterval+Extensions.swift
│ │ │ ├── NotificationName+Extensions.swift
│ │ │ └── URL+Extensions.swift
│ │ ├── HealthKit
│ │ │ ├── HKActivitySummary+Extensions.swift
│ │ │ └── HKWorkoutActivityType+Extensions.swift
│ │ ├── Std Lib
│ │ │ └── Dictionary+Extensions.swift
│ │ ├── StdLib
│ │ │ └── String+AnonymousName.swift
│ │ └── SwiftUI
│ │ │ ├── Binding+Extensions.swift
│ │ │ ├── Button+Extensions.swift
│ │ │ ├── ColorScheme+Extensions.swift
│ │ │ ├── Text+Extensions.swift
│ │ │ ├── View+Analytics.swift
│ │ │ ├── View+ConfirmationDialog.swift
│ │ │ ├── View+HUD.swift
│ │ │ └── View+RemovingMargin.swift
│ ├── Nonce
│ │ └── Nonce.swift
│ └── Utility+Factory.swift
└── Views
│ ├── CompetitionContainer
│ ├── Competition
│ │ ├── Competition+Banners.swift
│ │ ├── CompetitionParticipantRow.swift
│ │ ├── CompetitionView.swift
│ │ ├── CompetitionViewAction.swift
│ │ └── CompetitionViewModel.swift
│ ├── CompetitionContainerDateRange.swift
│ ├── CompetitionContainerDateRangeSelector.swift
│ ├── CompetitionContainerView.swift
│ ├── CompetitionContainerViewModel.swift
│ ├── Edit
│ │ ├── CompetitionEditView.swift
│ │ ├── CompetitionEditViewModel.swift
│ │ └── ScoringModelLearnMoreView.swift
│ └── Results
│ │ ├── CompetitionResultsDataPoint+View.swift
│ │ ├── CompetitionResultsDataPoint.swift
│ │ ├── CompetitionResultsView.swift
│ │ └── CompetitionResultsViewModel.swift
│ ├── Components
│ ├── ActivitySummaryInfo
│ │ ├── ActivitySummaryInfoSource.swift
│ │ ├── ActivitySummaryInfoView.swift
│ │ ├── ActivitySummaryInfoViewModel.swift
│ │ └── Health Kit Permissions Guide
│ │ │ ├── HealthKitPermissionsGuideView.swift
│ │ │ └── HealthKitPermissionsGuideViewModel.swift
│ ├── AppIcon.swift
│ ├── Banner.swift
│ ├── CompetitionDetails
│ │ ├── CompetitionDetails.swift
│ │ └── CompetitionDetailsViewModel.swift
│ ├── EmailSignIn
│ │ ├── EmailSignInView.swift
│ │ ├── EmailSignInViewInputType.swift
│ │ └── EmailSignInViewModel.swift
│ ├── FirebaseImage
│ │ ├── FirebaseImage.swift
│ │ └── FirebaseImageViewModel.swift
│ ├── GoogleAd
│ │ ├── GoogleAd.swift
│ │ ├── GoogleAdUnit.swift
│ │ └── GoogleAdViewModel.swift
│ ├── ImmutableListItemView.swift
│ ├── MedalsView.swift
│ ├── ProfilePicture
│ │ ├── ImageEditor.swift
│ │ └── ProfilePicture.swift
│ ├── SignInWithAppleButton.swift
│ ├── TextFieldWithSecureToggle.swift
│ ├── UIKitWrappers
│ │ └── ActivityRingView.swift
│ ├── UserHashIDPill.swift
│ └── UserInfoSection.swift
│ ├── Developer
│ ├── DeveloperMenu.swift
│ └── DeveloperMenuViewModel.swift
│ ├── Explore
│ ├── ExploreView.swift
│ ├── ExploreViewModel.swift
│ └── FeaturedCompetition.swift
│ ├── FeatureFlagOverride
│ ├── FeatureFlagOverrideView.swift
│ └── FeatureFlagOverrideViewModel.swift
│ ├── Home
│ ├── HomeView.swift
│ ├── HomeViewEmptyContent.swift
│ ├── HomeViewModel.swift
│ ├── Notifications
│ │ ├── NotificationsView.swift
│ │ └── NotificationsViewModel.swift
│ └── User
│ │ ├── UserView.swift
│ │ ├── UserViewAction.swift
│ │ └── UserViewModel.swift
│ ├── InviteFriends
│ ├── InviteFriendsAction.swift
│ ├── InviteFriendsView.swift
│ └── InviteFriendsViewModel.swift
│ ├── NavigationDestination.swift
│ ├── RootTab.swift
│ ├── RootView.swift
│ ├── RootViewModel.swift
│ ├── Settings
│ ├── Components
│ │ └── HideNameLearnMoreView.swift
│ ├── CreateAccount
│ │ ├── CreateAccountView.swift
│ │ └── CreateAccountViewModel.swift
│ ├── ReportIssue
│ │ ├── ReportIssueView.swift
│ │ └── ReportIssueViewModel.swift
│ ├── SettingsView.swift
│ └── SettingsViewModel.swift
│ ├── Styles
│ └── FlatLinkStyle.swift
│ └── Welcome
│ ├── VerifyEmail
│ ├── VerifyEmailView.swift
│ └── VerifyEmailViewModel.swift
│ ├── WelcomeNavigationDestination.swift
│ ├── WelcomeView.swift
│ └── WelcomeViewModel.swift
├── FriendlyCompetitionsTests
├── API
│ └── EndpointTests.swift
├── AppServices
│ ├── Background Jobs
│ │ └── BackgroundJobsAppServiceTests.swift
│ ├── Data Uploading
│ │ └── DataUploadingAppServiceTests.swift
│ └── Notifications
│ │ └── NotificationsAppServiceTests.swift
├── AppState
│ └── AppStateTests.swift
├── Extensions
│ └── StdLib
│ │ ├── ArrayTests.swift
│ │ ├── DecodableTests.swift
│ │ ├── EncodableTests.swift
│ │ ├── IntTests.swift
│ │ ├── SequenceTests.swift
│ │ └── StringTests.swift
├── Managers
│ ├── ActivitySummaryManagerTests.swift
│ ├── AuthenticationManagerTests.swift
│ ├── CompetitionsManagerTests.swift
│ ├── FriendsManagerTests.swift
│ ├── HealthKitManagerTests.swift
│ ├── SearchManagerTests.swift
│ ├── StepCountManagerTests.swift
│ └── WorkoutManagerTests.swift
├── Models
│ ├── ActivitySummary
│ │ └── ActivitySummaryTests.swift
│ ├── Competition
│ │ ├── Competition+ScoringModelTests.swift
│ │ ├── Competition+StandingTests.swift
│ │ └── CompetitionTests.swift
│ ├── Deep lInk
│ │ └── DeepLinkTests.swift
│ ├── Permissions
│ │ └── PermissionStatusTests.swift
│ ├── User
│ │ └── UserTests.swift
│ └── Workouts
│ │ ├── WorkoutTests.swift
│ │ └── WorkoutTypeTests.swift
├── Util
│ ├── Extensions
│ │ ├── ActivitySummary+Extensions.swift
│ │ ├── Foundation
│ │ │ ├── TimeIntervalTests.swift
│ │ │ └── UserDefaultsTests.swift
│ │ └── User+Extensions.swift
│ ├── FCTestCase.swift
│ ├── Mocks
│ │ ├── MockError.swift
│ │ └── SearchIndex+Mock.swift
│ └── Publisher+Extensions.swift
└── Views
│ ├── Competitions
│ ├── CompetitionParticipantRowConfigTests.swift
│ └── CompetitionViewModelTests.swift
│ ├── Components
│ ├── ActivitySummaryInfoViewModelTests.swift
│ ├── CompetitionDetailsViewModelTests.swift
│ ├── EmailSignInViewModelTests.swift
│ └── FirebaseImageViewModelTests.swift
│ ├── Explore
│ └── ExploreViewModelTests.swift
│ ├── Home
│ ├── HomeViewModelTests.swift
│ └── Profile
│ │ ├── Create Account
│ │ └── CreateAccountViewModelTests.swift
│ │ └── ProfileViewModelTests.swift
│ ├── Invite Friends
│ └── InviteFriendsViewModelTests.swift
│ ├── NavigationDestinationTests.swift
│ ├── RootViewModelTests.swift
│ └── Welcome
│ ├── Verify Email
│ └── VerifyEmailViewModelTests.swift
│ └── WelcomeViewModelTests.swift
├── FriendlyCompetitionsWidgets
├── CompetitionStandings
│ ├── CompetitionStandingsIntent.swift
│ ├── CompetitionStandingsWidget.swift
│ └── Models
│ │ ├── WidgetCompetition.swift
│ │ └── WidgetStanding.swift
├── FriendlyCompetitionsWidgetsBundle.swift
├── FriendlyCompetitionsWidgetsExtension.entitlements
├── Info.plist
├── Network
│ ├── AuthenticationAction.swift
│ ├── CollectionRequest.swift
│ ├── DocumentRequest.swift
│ ├── FirestoreDocument.swift
│ └── Network.swift
└── Resources
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── Contents.json
│ ├── WidgetBackground.colorset
│ │ └── Contents.json
│ └── icon.imageset
│ │ ├── Contents.json
│ │ └── ItunesArtwork@2x.png
│ └── Localizable.xcstrings
├── Gemfile
├── Gemfile.lock
├── README.md
├── fastlane
├── Appfile
├── Deliverfile
├── Fastfile
├── Matchfile
├── Pluginfile
├── README.md
├── lanes
│ ├── certificates.rb
│ ├── deploy.rb
│ └── test.rb
├── metadata
│ ├── copyright.txt
│ ├── en-CA
│ │ ├── description.txt
│ │ ├── keywords.txt
│ │ ├── marketing_url.txt
│ │ ├── name.txt
│ │ ├── privacy_url.txt
│ │ ├── release_notes.txt
│ │ ├── subtitle.txt
│ │ └── support_url.txt
│ ├── primary_category.txt
│ ├── review_information
│ │ ├── demo_password.txt
│ │ ├── demo_user.txt
│ │ ├── email_address.txt
│ │ ├── first_name.txt
│ │ ├── last_name.txt
│ │ ├── notes.txt
│ │ └── phone_number.txt
│ └── secondary_category.txt
├── screenshots
│ ├── README.txt
│ └── en-CA
│ │ ├── iPhone 11 Pro Max 1.png
│ │ ├── iPhone 11 Pro Max 2.png
│ │ ├── iPhone 11 Pro Max 3.png
│ │ ├── iPhone 11 Pro Max 4.png
│ │ ├── iPhone 8 Plus 1.png
│ │ ├── iPhone 8 Plus 2.png
│ │ ├── iPhone 8 Plus 3.png
│ │ └── iPhone 8 Plus 4.png
└── scripts
│ └── upload-symbols
├── firebase
├── .firebaserc
├── .gitignore
├── firebase.json
├── firestore.indexes.json
├── firestore.rules
├── functions
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── Handlers
│ │ │ ├── account
│ │ │ │ ├── accountSetup.ts
│ │ │ │ ├── deleteAccount.ts
│ │ │ │ └── signInWithAppleToken.ts
│ │ │ ├── competitions
│ │ │ │ ├── deleteCompetition.ts
│ │ │ │ ├── inviteUserToCompetition.ts
│ │ │ │ ├── joinCompetition.ts
│ │ │ │ ├── leaveCompetition.ts
│ │ │ │ ├── respondToCompetitionInvite.ts
│ │ │ │ ├── sendNewCompetitionInvites.ts
│ │ │ │ ├── updateCompetitionRanks.ts
│ │ │ │ └── updateCompetitionStandingsLEGACY.ts
│ │ │ ├── friends
│ │ │ │ ├── deleteFriend.ts
│ │ │ │ └── handleFriendRequest.ts
│ │ │ ├── jobs
│ │ │ │ ├── cleanupActivitySummaries.ts
│ │ │ │ ├── cleanupWorkouts.ts
│ │ │ │ ├── completeCompetitions.ts
│ │ │ │ ├── handleCompetitionCreate.ts
│ │ │ │ ├── handleCompetitionUpdate.ts
│ │ │ │ ├── updateActivitySummaryScores.ts
│ │ │ │ ├── updateScores.ts
│ │ │ │ ├── updateStepCountScores.ts
│ │ │ │ └── updateWorkoutScores.ts
│ │ │ ├── notifications
│ │ │ │ └── notifications.ts
│ │ │ └── standings
│ │ │ │ └── setStandingRanks.ts
│ │ ├── Models
│ │ │ ├── ActivitySummary.ts
│ │ │ ├── Competition.ts
│ │ │ ├── Helpers
│ │ │ │ └── EnumDictionary.ts
│ │ │ ├── ScoringModel.ts
│ │ │ ├── Standing.ts
│ │ │ ├── StepCount.ts
│ │ │ ├── User.ts
│ │ │ ├── Workout.ts
│ │ │ ├── WorkoutMetric.ts
│ │ │ └── WorkoutType.ts
│ │ ├── Utilities
│ │ │ ├── Constants.ts
│ │ │ ├── firestore.ts
│ │ │ ├── id.ts
│ │ │ └── prepareForFirestore.ts
│ │ └── index.ts
│ ├── tsconfig.dev.json
│ └── tsconfig.json
├── package-lock.json
├── package.json
└── scripts
│ ├── deploy.sh
│ ├── emulate.sh
│ ├── lint.sh
│ └── ports.sh
├── icon
├── ios
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Icon-App-20x20@1x.png
│ │ ├── Icon-App-20x20@2x.png
│ │ ├── Icon-App-20x20@3x.png
│ │ ├── Icon-App-29x29@1x.png
│ │ ├── Icon-App-29x29@2x.png
│ │ ├── Icon-App-29x29@3x.png
│ │ ├── Icon-App-40x40@1x.png
│ │ ├── Icon-App-40x40@2x.png
│ │ ├── Icon-App-40x40@3x.png
│ │ ├── Icon-App-60x60@2x.png
│ │ ├── Icon-App-60x60@3x.png
│ │ ├── Icon-App-76x76@1x.png
│ │ ├── Icon-App-76x76@2x.png
│ │ ├── Icon-App-83.5x83.5@2x.png
│ │ └── ItunesArtwork@2x.png
│ ├── iTunesArtwork@1x.png
│ ├── iTunesArtwork@2x.png
│ └── iTunesArtwork@3x.png
└── square.png
├── screenshots
├── 6.5
│ ├── competition.jpeg
│ ├── explore.jpeg
│ ├── home.jpeg
│ └── new_competition.jpeg
└── mockups
│ ├── Friendly Competitions.mockup
│ └── studio.app-mockup.com
├── sourcery
└── templates
│ └── AutoMockable.stencil
└── swiftgen.yml
/.github/workflows/deploy-firebase.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Firebase
2 | on:
3 | push:
4 | branches:
5 | - main
6 | paths:
7 | - 'firebase/**'
8 |
9 | jobs:
10 | build:
11 | name: Deploy
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@latest
16 | - name: Install firebase-tools
17 | run: npm install -g firebase-tools
18 | - name: Install dependencies
19 | run: cd firebase && npm install
20 | - name: Install functions dependencies
21 | run: cd firebase/functions && npm install
22 | - name: Build & deploy
23 | run: cd firebase && firebase deploy --token "$FIREBASE_TOKEN" --non-interactive
24 | env:
25 | FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
26 |
--------------------------------------------------------------------------------
/.github/workflows/test-ios.yml:
--------------------------------------------------------------------------------
1 | name: Run unit tests
2 |
3 | on:
4 | pull_request:
5 | branches: main
6 | paths:
7 | - FriendlyCompetitions.xcodeproj/**
8 | - FriendlyCompetitions/**
9 | - FriendlyCompetitionsTests/**
10 | - FriendlyCompetitionsWidgets/**
11 | - FCKit/**
12 |
13 | concurrency:
14 | group: ${{ github.ref }}
15 | cancel-in-progress: true
16 |
17 | jobs:
18 | build:
19 | name: Test
20 | runs-on: macos-15
21 | env:
22 | BUILDCACHE_DEBUG: 2
23 | steps:
24 | - name: Checkout
25 | uses: actions/checkout@latest
26 | with:
27 | fetch-depth: 0
28 |
29 | - name: Setup SSH
30 | uses: webfactory/ssh-agent@latest
31 | with:
32 | ssh-private-key: ${{ secrets.SSH_KEY }}
33 |
34 | - name: Cache Swift Packages
35 | uses: actions/cache@latest
36 | with:
37 | path: ~/Library/Developer/Xcode/DerivedData/**/SourcePackages/
38 | key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
39 | restore-keys: |
40 | ${{ runner.os }}-spm-
41 |
42 | - name: Setup Ruby
43 | uses: ruby/setup-ruby@latest
44 | with:
45 | bundler-cache: true
46 |
47 | - name: Setup GoogleService-Info.plist
48 | run: |
49 | mkdir -p "FriendlyCompetitions/Firebase/Debug"
50 | echo $GOOGLE_SERVICE_INFO_DEBUG > "FriendlyCompetitions/Firebase/Debug/GoogleService-Info.plist"
51 | env:
52 | GOOGLE_SERVICE_INFO_DEBUG: ${{ secrets.GOOGLE_SERVICE_INFO_DEBUG }}
53 |
54 | - name: Homebrew
55 | run: brew bundle
56 |
57 | - name: Fastlane
58 | run: bundle exec fastlane test
59 | env:
60 | FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120
61 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.7.3
--------------------------------------------------------------------------------
/.sourcery.yml:
--------------------------------------------------------------------------------
1 | sources:
2 | - FriendlyCompetitions
3 | templates:
4 | - sourcery/templates
5 | output:
6 | FriendlyCompetitions/Sourcery
7 | args:
8 | autoMockableImports:
9 | - Combine
10 | - HealthKit
11 |
12 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": [
3 | "requestee"
4 | ]
5 | }
--------------------------------------------------------------------------------
/Brewfile:
--------------------------------------------------------------------------------
1 | # brew "swiftgen" # swiftgen is currently failing to install
2 | brew "sourcery"
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Setup for contributors
2 | ## Homebrew
3 | Instal [homebrew](https://brew.sh) and run the following command:
4 | ```shell
5 | brew bundle
6 | ```
7 | This command will need to be re-run everytime [Brewfile](Brewfile) changes (almost never)
8 |
9 | ## Firebase
10 | Access to a Firebase project is required. You can create one for youself in the [firebase console](https://console.firebase.google.com). Follow the instructions to setup a project and download the `GoogleService-Info.plist`. You'll need to place it in one of the following directories, depending on the scheme that you plan to run:
11 | - `Friendly Competitions/Firebase/Debug/`
12 | - `Friendly Competitions/Firebase/Release/`
13 |
14 | The app uses the following firebase services:
15 |
16 | ### [Authentication](https://firebase.google.com/docs/auth)
17 | Manages user authentication. Users need to be signed in to access any & all features
18 | - Apple
19 | - Email
20 |
21 | ### [Firestore](https://firebase.google.com/docs/firestore)
22 | Stores all user & competition data
23 |
24 | ### [Functions](https://firebase.google.com/docs/functions)
25 | Sends notifications, computes competition scores, cleans up old data.
26 |
27 | To deploy this project's functions, run the following commands:
28 | ```
29 | cd firebase/functions
30 | npm install
31 | npm run-script deploy
32 | ```
33 | > Note: To deploy functions, this service requires the [Blaze plan](https://firebase.google.com/pricing). You can also explore [emulating functions](https://firebase.google.com/docs/functions/get-started#emulate-execution-of-your-functions) instead of upgrading to the Blaze plan, or disable it's usage in the iOS client.
34 |
35 | ### [Storage](https://firebase.google.com/docs/storage)
36 | Stores images, nothing else for now
37 |
--------------------------------------------------------------------------------
/FCKit/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/FCKit/.sourcery.yml:
--------------------------------------------------------------------------------
1 | sources:
2 | - Sources/FCKit
3 | templates:
4 | - sourcery/templates
5 | output:
6 | Sources/FCKitMocks
7 | args:
8 | autoMockableImports:
9 | - Combine
10 | - FCKit
11 |
--------------------------------------------------------------------------------
/FCKit/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/FCKit/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "FCKit",
7 | platforms: [.iOS(.v16)],
8 | products: [
9 | .library(name: "FCKit", targets: ["FCKit"]),
10 | .library(name: "FCKitMocks", targets: ["FCKitMocks"])
11 | ],
12 | dependencies: [
13 | .package(url: "https://github.com/CombineCommunity/CombineExt", from: "1.0.0"),
14 | .package(url: "https://github.com/EvanCooper9/ECKit", branch: "main"),
15 | .package(url: "https://github.com/hmlongco/Factory", from: "2.0.0"),
16 | .package(url: "https://github.com/firebase/firebase-ios-sdk", from: "11.0.0"),
17 | ],
18 | targets: [
19 | .target(name: "FCKit", dependencies: [
20 | "CombineExt",
21 | "ECKit",
22 | "Factory",
23 | .product(name: "FirebaseAnalytics", package: "firebase-ios-sdk"),
24 | .product(name: "FirebaseCrashlytics", package: "firebase-ios-sdk"),
25 | .product(name: "FirebaseFirestore", package: "firebase-ios-sdk"),
26 | .product(name: "FirebaseFirestoreCombine-Community", package: "firebase-ios-sdk"),
27 | .product(name: "FirebaseRemoteConfig", package: "firebase-ios-sdk"),
28 | ]),
29 | .target(name: "FCKitMocks", dependencies: [
30 | "FCKit",
31 | .product(name: "FirebaseAuth", package: "firebase-ios-sdk"),
32 | .product(name: "FirebaseFirestore", package: "firebase-ios-sdk"),
33 | ]),
34 | .testTarget(name: "FCKitTests", dependencies: [
35 | "FCKit",
36 | "FCKitMocks"
37 | ]),
38 | ]
39 | )
40 |
--------------------------------------------------------------------------------
/FCKit/Sources/FCKit/Analytics/AnalyticsManager+Factory.swift:
--------------------------------------------------------------------------------
1 | import Factory
2 |
3 | public extension Container {
4 | var analyticsManager: Factory {
5 | self { AnalyticsManager() }.scope(.shared)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/FCKit/Sources/FCKit/Database/Settings/DatabaseSettingsManager.swift:
--------------------------------------------------------------------------------
1 | import Factory
2 | import Foundation
3 |
4 | // sourcery: AutoMockable
5 | public protocol DatabaseSettingManaging {
6 | var shouldResetCache: Bool { get }
7 | func didResetCache()
8 | }
9 |
10 | final class DatabaseSettingsManager: DatabaseSettingManaging {
11 |
12 | // MARK: - Private Properties
13 |
14 | @Injected(\.databaseSettingsStore) private var databaseSettingsStore
15 | @Injected(\.featureFlagManager) private var featureFlagManager
16 |
17 | // MARK: - Public Properties
18 |
19 | var shouldResetCache: Bool {
20 | let ttl = featureFlagManager.value(forDouble: .databaseCacheTtl)
21 | return databaseSettingsStore.lastCacheReset.addingTimeInterval(ttl) <= .now
22 | }
23 |
24 | // MARK: - Public Methods
25 |
26 | func didResetCache() {
27 | databaseSettingsStore.lastCacheReset = .now
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/FCKit/Sources/FCKit/Database/Settings/DatabaseSettingsStore.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol DatabaseSettingsStoring {
4 | var lastCacheReset: Date { get set }
5 | }
6 |
7 | extension UserDefaults: DatabaseSettingsStoring {
8 |
9 | private enum Constants {
10 | static var lastCacheResetKey: String { #function }
11 | }
12 |
13 | public var lastCacheReset: Date {
14 | get { decode(Date.self, forKey: Constants.lastCacheResetKey) ?? .now }
15 | set { encode(newValue, forKey: Constants.lastCacheResetKey) }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/FCKit/Sources/FCKit/Environment/EnvironmentCache.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // sourcery: AutoMockable
4 | public protocol EnvironmentCache {
5 | var environment: FCEnvironment? { get set }
6 | }
7 |
8 | extension UserDefaults: EnvironmentCache {
9 |
10 | private enum Constants {
11 | static var environmentKey: String { #function }
12 | }
13 |
14 | public var environment: FCEnvironment? {
15 | get { decode(FCEnvironment.self, forKey: Constants.environmentKey) }
16 | set { encode(newValue, forKey: Constants.environmentKey) }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/FCKit/Sources/FCKit/Environment/EnvironmentManager+Factory.swift:
--------------------------------------------------------------------------------
1 | import Factory
2 | import Foundation
3 |
4 | public extension Container {
5 | var environmentCache: Factory {
6 | self { UserDefaults.appGroup }.scope(.shared)
7 | }
8 |
9 | var environmentManager: Factory {
10 | self { EnvironmentManager() }.scope(.singleton)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/FCKit/Sources/FCKit/Environment/EnvironmentManager.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import CombineExt
3 | import ECKit
4 | import Factory
5 | import Foundation
6 |
7 | // sourcery: AutoMockable
8 | public protocol EnvironmentManaging {
9 | var environment: FCEnvironment { get }
10 | var environmentPublisher: AnyPublisher { get }
11 | func set(_ environment: FCEnvironment)
12 | }
13 |
14 | final class EnvironmentManager: EnvironmentManaging {
15 |
16 | private enum Constants {
17 | static var environmentKey: String { #function }
18 | }
19 |
20 | // MARK: - Public Properties
21 |
22 | var environment: FCEnvironment {
23 | environmentSubject.value
24 | }
25 |
26 | var environmentPublisher: AnyPublisher {
27 | environmentSubject.eraseToAnyPublisher()
28 | }
29 |
30 | // MARK: - Private Properties
31 |
32 | @Injected(\.environmentCache) private var environmentCache
33 |
34 | private let environmentSubject = CurrentValueSubject(.prod)
35 | private var cancellables = Cancellables()
36 |
37 | // MARK: - Lifecycle
38 |
39 | init() {
40 | if let environment = environmentCache.environment {
41 | environmentSubject.send(environment)
42 | } else {
43 | #if targetEnvironment(simulator)
44 | environmentSubject.send(.debugLocal)
45 | #endif
46 | }
47 |
48 | environmentSubject
49 | .sink(withUnretained: self) { $0.environmentCache.environment = $1 }
50 | .store(in: &cancellables)
51 | }
52 |
53 | // MARK: - Public Methods
54 |
55 | func set(_ environment: FCEnvironment) {
56 | environmentSubject.send(environment)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/FCKit/Sources/FCKit/Environment/FCEnvironment.swift:
--------------------------------------------------------------------------------
1 | public enum FCEnvironment: Codable, Equatable {
2 | case prod
3 | case debugLocal
4 | case debugRemote(destination: String)
5 |
6 | public var isDebug: Bool {
7 | switch self {
8 | case .prod:
9 | return false
10 | case .debugLocal, .debugRemote:
11 | return true
12 | }
13 | }
14 |
15 | public var bundleIdentifier: String {
16 | switch self {
17 | case .prod:
18 | return "com.evancooper.FriendlyCompetitions"
19 | case .debugLocal:
20 | return "com.evancooper.FriendlyCompetitions.debug"
21 | case .debugRemote:
22 | return "com.evancooper.FriendlyCompetitions.debug"
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/FCKit/Sources/FCKit/Extensions/Combine/Publisher+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Evan Cooper on 2024-02-19.
6 | //
7 |
8 | import Foundation
9 |
--------------------------------------------------------------------------------
/FCKit/Sources/FCKit/Extensions/Foundation/UserDefaults+Extensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public extension UserDefaults {
4 | static var appGroup: UserDefaults {
5 | UserDefaults(suiteName: AppGroup.id()) ?? .standard
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/FCKit/Sources/FCKit/Extensions/StdLib/Error+Extensions.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import FirebaseCrashlytics
3 |
4 | extension Error {
5 | public func reportToCrashlytics(userInfo: [String: Any] = [:], file: String = #file, line: Int = #line) {
6 | var nsError = self as NSError
7 |
8 | var userInfo = nsError.userInfo.merging(userInfo) { _, newValue in newValue }
9 | userInfo["file"] = file
10 | userInfo["line"] = line
11 |
12 | nsError = NSError(
13 | domain: nsError.domain,
14 | code: nsError.code,
15 | userInfo: nsError.userInfo.merging(userInfo) { _, newValue in newValue }
16 | )
17 |
18 | Crashlytics.crashlytics().record(error: nsError)
19 | }
20 | }
21 |
22 | extension Publisher where Failure == Error {
23 | public func reportErrorToCrashlytics(userInfo: [String: Any] = [:], caller: String = #function, file: String = #file, line: Int = #line) -> AnyPublisher