Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ jobs:
p12-file-base64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
p12-password: ${{ secrets.P12_PASSWORD }}

- name: Write Firebase config
env:
FIREBASE_MACOS_B64: ${{ secrets.FIREBASE_MACOS_B64 }}
run: ./scripts/ci/write-firebase-config.sh

- name: Bump version (release only)
if: github.event_name == 'release'
run: |
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ jobs:
- name: Install xcpretty
run: gem install xcpretty

- name: Write Firebase config
env:
FIREBASE_MACOS_B64: ${{ secrets.FIREBASE_MACOS_B64 }}
run: ./scripts/ci/write-firebase-config.sh

- name: Run Xcode tests
run: |
set -o pipefail && xcodebuild \
Expand Down Expand Up @@ -90,6 +95,11 @@ jobs:
- name: Install xcpretty
run: gem install xcpretty

- name: Write Firebase config
env:
FIREBASE_MACOS_B64: ${{ secrets.FIREBASE_MACOS_B64 }}
run: ./scripts/ci/write-firebase-config.sh

- name: Build (no signing)
run: |
set -o pipefail && xcodebuild \
Expand All @@ -116,6 +126,11 @@ jobs:
- name: Install xcpretty
run: gem install xcpretty

- name: Write Firebase config
env:
FIREBASE_IOS_B64: ${{ secrets.FIREBASE_IOS_B64 }}
run: ./scripts/ci/write-firebase-config.sh

- name: Build mobile app (ad-hoc signing)
run: |
# The iOS app embeds extensions (notification service, widget), so
Expand Down Expand Up @@ -163,6 +178,11 @@ jobs:
- name: Install xcpretty
run: gem install xcpretty

- name: Write Firebase config
env:
FIREBASE_IOS_B64: ${{ secrets.FIREBASE_IOS_B64 }}
run: ./scripts/ci/write-firebase-config.sh

- name: Resolve simulator
id: sim
run: |
Expand Down Expand Up @@ -233,6 +253,11 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Write Firebase config
env:
FIREBASE_ANDROID_B64: ${{ secrets.FIREBASE_ANDROID_B64 }}
run: ./scripts/ci/write-firebase-config.sh

- name: Assemble debug APK
working-directory: RxCodeAndroid
run: ./gradlew assembleDebug --stacktrace
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,8 @@ relay-server/.env.*
relay-server/*.p8
relay-server/k8s/secrets.yaml
.playwright-mcp

# Firebase config files (decoded from CI secrets, never committed)
RxCode/GoogleService-Info.plist
RxCodeMobile/GoogleService-Info.plist
RxCodeAndroid/app/google-services.json
50 changes: 50 additions & 0 deletions Packages/Sources/RxCodeCore/Utilities/AnalyticsService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Foundation

/// Lightweight, transport-agnostic analytics dispatch shared by macOS, iOS, and
/// any future hosts. App targets install a handler at launch (typically wired
/// to Firebase Analytics) and view code calls `AnalyticsService.shared.log(_:)`
/// without taking a Firebase dependency on `RxCodeCore`.
public final class AnalyticsService: @unchecked Sendable {
public static let shared = AnalyticsService()

public typealias Handler = (_ name: String, _ parameters: [String: Any]?) -> Void

private let queue = DispatchQueue(label: "com.rxlab.rxcode.analytics")
private var handler: Handler?

private init() {}

public func install(handler: @escaping Handler) {
queue.sync { self.handler = handler }
}

public func log(_ event: AnalyticsEvent, parameters: [String: Any]? = nil) {
log(name: event.rawValue, parameters: parameters)
}

public func log(name: String, parameters: [String: Any]? = nil) {
let handler = queue.sync { self.handler }
handler?(name, parameters)
}
}

/// Canonical names for the custom events emitted by the apps. Keeping them in
/// one enum avoids drift between platforms and keeps Firebase dashboards tidy.
public enum AnalyticsEvent: String, Sendable {
// Navigation
case briefingListOpened = "briefing_list_opened"
case briefingDetailOpened = "briefing_detail_opened"
case threadOpened = "thread_opened"
case threadCreated = "thread_created"
case projectOpened = "project_opened"

// Feature surfaces
case diffViewOpened = "diff_view_opened"
case runProfileEditorOpened = "run_profile_editor_opened"
case runProfileExecuted = "run_profile_executed"
case inAppBrowserOpened = "in_app_browser_opened"

// Misc
case settingsOpened = "settings_opened"
case newProjectStarted = "new_project_started"
}
17 changes: 17 additions & 0 deletions Packages/Sources/RxCodeSync/Protocol/Payload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum Payload: Sendable {
case pairAck(PairAckPayload)
case unpair(UnpairPayload)
case apnsToken(APNsTokenPayload)
case pushToken(PushTokenPayload)
case liveActivityToken(LiveActivityTokenPayload)
case requestSnapshot(RequestSnapshotPayload)
case snapshot(SnapshotPayload)
Expand Down Expand Up @@ -75,6 +76,7 @@ public extension Payload {
case .pairAck: return "pair_ack"
case .unpair: return "unpair"
case .apnsToken: return "apns_token"
case .pushToken: return "push_token"
case .liveActivityToken: return "live_activity_token"
case .requestSnapshot: return "request_snapshot"
case .snapshot: return "snapshot"
Expand Down Expand Up @@ -184,6 +186,18 @@ public struct APNsTokenPayload: Codable, Sendable {
}
}

public struct PushTokenPayload: Codable, Sendable {
public let provider: String
public let token: String
public let environment: String?

public init(provider: String, token: String, environment: String? = nil) {
self.provider = provider
self.token = token
self.environment = environment
}
}

/// Mobile → desktop: ActivityKit push tokens for the job Live Activity. A
/// single payload reports either the device-wide push-to-start token, a
/// per-activity update token, or both. The desktop stores them per paired
Expand Down Expand Up @@ -675,6 +689,7 @@ extension Payload: Codable {
case pairAck = "pair_ack"
case unpair
case apnsToken = "apns_token"
case pushToken = "push_token"
case liveActivityToken = "live_activity_token"
case requestSnapshot = "request_snapshot"
case snapshot
Expand Down Expand Up @@ -743,6 +758,7 @@ extension Payload: Codable {
case .pairAck: self = .pairAck(try container.decode(PairAckPayload.self, forKey: .data))
case .unpair: self = .unpair(try container.decode(UnpairPayload.self, forKey: .data))
case .apnsToken: self = .apnsToken(try container.decode(APNsTokenPayload.self, forKey: .data))
case .pushToken: self = .pushToken(try container.decode(PushTokenPayload.self, forKey: .data))
case .liveActivityToken: self = .liveActivityToken(try container.decode(LiveActivityTokenPayload.self, forKey: .data))
case .requestSnapshot: self = .requestSnapshot(try container.decode(RequestSnapshotPayload.self, forKey: .data))
case .snapshot: self = .snapshot(try container.decode(SnapshotPayload.self, forKey: .data))
Expand Down Expand Up @@ -807,6 +823,7 @@ extension Payload: Codable {
case .pairAck(let p): try container.encode(TypeKey.pairAck.rawValue, forKey: .type); try container.encode(p, forKey: .data)
case .unpair(let p): try container.encode(TypeKey.unpair.rawValue, forKey: .type); try container.encode(p, forKey: .data)
case .apnsToken(let p): try container.encode(TypeKey.apnsToken.rawValue, forKey: .type); try container.encode(p, forKey: .data)
case .pushToken(let p): try container.encode(TypeKey.pushToken.rawValue, forKey: .type); try container.encode(p, forKey: .data)
case .liveActivityToken(let p): try container.encode(TypeKey.liveActivityToken.rawValue, forKey: .type); try container.encode(p, forKey: .data)
case .requestSnapshot(let p): try container.encode(TypeKey.requestSnapshot.rawValue, forKey: .type); try container.encode(p, forKey: .data)
case .snapshot(let p): try container.encode(TypeKey.snapshot.rawValue, forKey: .type); try container.encode(p, forKey: .data)
Expand Down
18 changes: 18 additions & 0 deletions Packages/Tests/RxCodeSyncTests/PayloadTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,24 @@ struct PayloadTests {
#expect(request.apnsEnvironment == "sandbox")
}

@Test("push token carries provider and token")
func pushTokenCarriesProviderAndToken() throws {
let payload = Payload.pushToken(
PushTokenPayload(provider: "fcm", token: "fcm-token")
)

let data = try JSONEncoder().encode(payload)
let decoded = try JSONDecoder().decode(Payload.self, from: data)
guard case .pushToken(let token) = decoded else {
Issue.record("Expected push token payload")
return
}

#expect(token.provider == "fcm")
#expect(token.token == "fcm-token")
#expect(token.environment == nil)
}

@Test("snapshot carries briefing and settings data")
func snapshotCarriesBriefingAndSettingsData() throws {
let projectId = UUID(uuidString: "11111111-2222-3333-4444-555555555555")!
Expand Down
41 changes: 41 additions & 0 deletions RxCode.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
E6D001032FA0000100000001 /* RxCodeCore in Frameworks */ = {isa = PBXBuildFile; productRef = E6D001012FA0000100000001 /* RxCodeCore */; };
E6D001042FA0000100000001 /* RxCodeChatKit in Frameworks */ = {isa = PBXBuildFile; productRef = E6D001022FA0000100000001 /* RxCodeChatKit */; };
E6D001072FA0000100000001 /* RxCodeSync in Frameworks */ = {isa = PBXBuildFile; productRef = E6D001062FA0000100000001 /* RxCodeSync */; };
FB0000030000000000000001 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = FB0000010000000000000001 /* FirebaseAnalytics */; };
FB0000040000000000000001 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = FB0000020000000000000001 /* FirebaseCrashlytics */; };
FB0000070000000000000001 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = FB0000050000000000000001 /* FirebaseAnalytics */; };
FB0000080000000000000001 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = FB0000060000000000000001 /* FirebaseCrashlytics */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -244,6 +248,8 @@
DF230BA32FBC73BA008929A6 /* RxCodeSync in Frameworks */,
DF230BA12FBC73BA008929A6 /* RxCodeCore in Frameworks */,
DF230B9F2FBC73BA008929A6 /* RxCodeChatKit in Frameworks */,
FB0000070000000000000001 /* FirebaseAnalytics in Frameworks */,
FB0000080000000000000001 /* FirebaseCrashlytics in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -283,6 +289,8 @@
E6D001042FA0000100000001 /* RxCodeChatKit in Frameworks */,
E6D001072FA0000100000001 /* RxCodeSync in Frameworks */,
DF23FF1D2FBB42F7008929A6 /* WaterfallGrid in Frameworks */,
FB0000030000000000000001 /* FirebaseAnalytics in Frameworks */,
FB0000040000000000000001 /* FirebaseCrashlytics in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -459,6 +467,8 @@
DF230B9E2FBC73BA008929A6 /* RxCodeChatKit */,
DF230BA02FBC73BA008929A6 /* RxCodeCore */,
DF230BA22FBC73BA008929A6 /* RxCodeSync */,
FB0000050000000000000001 /* FirebaseAnalytics */,
FB0000060000000000000001 /* FirebaseCrashlytics */,
);
productName = RxCodeMobile;
productReference = DF230B4F2FBC7367008929A6 /* RxCodeMobile.app */;
Expand Down Expand Up @@ -560,6 +570,8 @@
E6D001022FA0000100000001 /* RxCodeChatKit */,
E6D001062FA0000100000001 /* RxCodeSync */,
DF23FF1C2FBB42F7008929A6 /* WaterfallGrid */,
FB0000010000000000000001 /* FirebaseAnalytics */,
FB0000020000000000000001 /* FirebaseCrashlytics */,
);
productName = RxCode;
productReference = E67335382F7356F600FD26C7 /* RxCode.app */;
Expand Down Expand Up @@ -614,6 +626,7 @@
E6D001002FA0000100000001 /* XCLocalSwiftPackageReference "Packages" */,
DF06CCCF2FB4CAB5005991E1 /* XCRemoteSwiftPackageReference "ViewInspector" */,
DF23FF1B2FBB42F7008929A6 /* XCRemoteSwiftPackageReference "WaterfallGrid" */,
FB0000000000000000000001 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = E67335392F7356F600FD26C7 /* Products */;
Expand Down Expand Up @@ -1466,6 +1479,14 @@
minimumVersion = 2.0.0;
};
};
FB0000000000000000000001 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/firebase/firebase-ios-sdk";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 11.4.0;
};
};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
Expand Down Expand Up @@ -1547,6 +1568,26 @@
package = E6D001002FA0000100000001 /* XCLocalSwiftPackageReference "Packages" */;
productName = RxCodeSync;
};
FB0000010000000000000001 /* FirebaseAnalytics */ = {
isa = XCSwiftPackageProductDependency;
package = FB0000000000000000000001 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseAnalytics;
};
FB0000020000000000000001 /* FirebaseCrashlytics */ = {
isa = XCSwiftPackageProductDependency;
package = FB0000000000000000000001 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseCrashlytics;
};
FB0000050000000000000001 /* FirebaseAnalytics */ = {
isa = XCSwiftPackageProductDependency;
package = FB0000000000000000000001 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseAnalytics;
};
FB0000060000000000000001 /* FirebaseCrashlytics */ = {
isa = XCSwiftPackageProductDependency;
package = FB0000000000000000000001 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseCrashlytics;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = E67335302F7356F600FD26C7 /* Project object */;
Expand Down
Loading
Loading