diff --git a/CHANGELOG.md b/CHANGELOG.md index 04eeea2a2..304d756b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added function for creating a new list and a test verifying list editing. [#112](https://github.com/verse-pbc/issues/issues/112) - Localized strings on the feed filter drop-down view. - Disabled automatic tracking in Sentry. [#126](https://github.com/verse-pbc/issues/issues/126) +- Track TestFlight vs AppStore installations in Posthog. [#130](https://github.com/verse-pbc/issues/issues/130) - Added functionality to get follows notifications in the Notifications tab. [#127](https://github.com/verse-pbc/issues/issues/127) ## [1.1] - 2025-01-03Z diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e14ab2151..7f36b5cb2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,15 @@ A maintainer will review your code and merge it when it has the required number ## Hot Reloading -We make use of the [Inject](https://github.com/krzysztofzablocki/Inject) framework for hot reloading debug builds. To set up hot reloading, follow the [documentation](https://github.com/krzysztofzablocki/Inject?tab=readme-ov-file#individual-developer-setup-once-per-machine). +We make use of the [Inject](https://github.com/krzysztofzablocki/Inject) framework for hot reloading debug builds. To set it up install the latest version of [InjectionIII](https://github.com/johnno1962/InjectionIII/releases). You can hot reload the app by: +- Launching InjectionIII +- Add `import Inject`, `@ObserveInjection var inject` to the top of the SwiftUI view you wish to reload, and add `.enableInjection()` as the last line in `body`. +- Build and run the app. You should see something like `💉 InjectionIII connected /Users/you/nos/Nos.xcodeproj` in the console. +- Change some code. +- Hit command-S to save. You should see Inject recompile the file in the logs +- For some reason our views don't update right away, but if you navigate away from the screen and back it should have reloaded. + +Full documentation is availabe [here](https://github.com/krzysztofzablocki/Inject?tab=readme-ov-file#workflow-integration) ## Dependency Management diff --git a/Nos/AppController.swift b/Nos/AppController.swift index f46b6846a..f920a2d2a 100644 --- a/Nos/AppController.swift +++ b/Nos/AppController.swift @@ -20,6 +20,7 @@ import Logger init() { currentState = .loading Log.info("App Version: \(Bundle.current.versionAndBuild)") + analytics.trackInstallationSourceIfNeeded() } func configureCurrentState() { diff --git a/Nos/Assets/Localization/Localizable.xcstrings b/Nos/Assets/Localization/Localizable.xcstrings index 7c5d65526..c494320da 100644 --- a/Nos/Assets/Localization/Localizable.xcstrings +++ b/Nos/Assets/Localization/Localizable.xcstrings @@ -14084,7 +14084,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your **@username** is your identity in the Nos community.
\nChoose a name that reflects you or your organization. Make it memorable and distinct!" + "value" : "Your **@username** is your identity in the Nos community.\u2028\nChoose a name that reflects you or your organization. Make it memorable and distinct!" } }, "es" : { @@ -19342,7 +19342,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Well done, you've successfully claimed your **@username**!
\nYou can share this name with other people in the Nostr and Fediverse communities to make it easy to find you." + "value" : "Well done, you've successfully claimed your **@username**!\u2028\nYou can share this name with other people in the Nostr and Fediverse communities to make it easy to find you." } }, "es" : { diff --git a/Nos/Extensions/Bundle+Current.swift b/Nos/Extensions/Bundle+Current.swift index 71d9177f8..f8b2fdf22 100644 --- a/Nos/Extensions/Bundle+Current.swift +++ b/Nos/Extensions/Bundle+Current.swift @@ -3,6 +3,11 @@ import Foundation private class CurrentBundle {} extension Bundle { + enum InstallationSource: String { + case testFlight = "TestFlight" + case appStore = "App Store" + case debug = "Debug" + } static let current = Bundle(for: CurrentBundle.self) @@ -21,4 +26,23 @@ extension Bundle { var versionAndBuild: String { "\(self.version) (\(self.build))" } + + /// > Warning: This method relies on undocumented implementation details to determine the installation source + /// and may break in future iOS releases. + /// https://gist.github.com/lukaskubanek/cbfcab29c0c93e0e9e0a16ab09586996 + /// Checks the app's receipt URL to determine if it contains the TestFlight-specific + /// "sandboxReceipt" identifier. + /// - Returns: `true` if the app was installed through TestFlight, `false` otherwise. + private var isTestFlight: Bool { + Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" + } + + /// Returns the app's installation source: debug, TestFlight, or App Store. + var installationSource: InstallationSource { + #if DEBUG + return .debug + #else + return isTestFlight ? .testFlight : .appStore + #endif + } } diff --git a/Nos/Extensions/UIDevice+Simulator.swift b/Nos/Extensions/UIDevice+Simulator.swift index 5ae8daea2..2bfdd9670 100644 --- a/Nos/Extensions/UIDevice+Simulator.swift +++ b/Nos/Extensions/UIDevice+Simulator.swift @@ -6,4 +6,12 @@ extension UIDevice { static var isSimulator: Bool { ProcessInfo.processInfo.environment["SIMULATOR_DEVICE_NAME"] != nil } + + static var platformName: String { + #if os(iOS) + return UIDevice.current.systemName + #elseif os(macOS) + return "macOS" + #endif + } } diff --git a/Nos/Service/Analytics.swift b/Nos/Service/Analytics.swift index 945396d5e..c781eaa67 100644 --- a/Nos/Service/Analytics.swift +++ b/Nos/Service/Analytics.swift @@ -1,4 +1,4 @@ -import Foundation +import UIKit import PostHog import Dependencies import Logger @@ -155,6 +155,31 @@ class Analytics { postHog?.capture(eventName, properties: properties) } + /// Tracks the source of the app download when the user launches the app. + func trackInstallationSourceIfNeeded() { + let source = Bundle.main.installationSource + // Make sure we don't track in debug mode. + guard source != .debug else { return } + + let installSourceKey = "TrackedAppInstallationSource" + + // Check if we've already tracked this installation. + if UserDefaults.standard.bool(forKey: installSourceKey) { + return + } + + track( + "Installation Source", + properties: [ + "source": source.rawValue, + "platform": UIDevice.platformName, + "app_version": Bundle.current.versionAndBuild + ] + ) + // Mark as tracked so we don't track again + UserDefaults.standard.set(true, forKey: installSourceKey) + } + /// Tracks when the user submits a search on the Discover screen. func searchedDiscover() { track("Discover Search Started")