updated privacy
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
ade1eb342a
commit
63b583ebe2
@ -29,12 +29,11 @@
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
EACONFIG005000000000000 /* Baccarat/Configuration/Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Baccarat/Configuration/Debug.xcconfig; sourceTree = SOURCE_ROOT; };
|
||||
EACONFIG006000000000000 /* Baccarat/Configuration/Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Baccarat/Configuration/Release.xcconfig; sourceTree = SOURCE_ROOT; };
|
||||
EAD890B72EF1E9CE006DBA80 /* Baccarat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Baccarat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EAD890C42EF1E9CF006DBA80 /* BaccaratTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BaccaratTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EAD890CE2EF1E9CF006DBA80 /* BaccaratUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BaccaratUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
EACONFIG004000000000000 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Baccarat/Configuration/Base.xcconfig; sourceTree = SOURCE_ROOT; };
|
||||
EACONFIG005000000000000 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Baccarat/Configuration/Debug.xcconfig; sourceTree = SOURCE_ROOT; };
|
||||
EACONFIG006000000000000 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Baccarat/Configuration/Release.xcconfig; sourceTree = SOURCE_ROOT; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
@ -82,6 +81,15 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
EA39BCA72F1D35690073D1E1 /* Recovered References */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EACONFIG005000000000000 /* Baccarat/Configuration/Debug.xcconfig */,
|
||||
EACONFIG006000000000000 /* Baccarat/Configuration/Release.xcconfig */,
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EA5AD1FF2EF34B660040CB90 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -97,6 +105,7 @@
|
||||
EAD890D12EF1E9CF006DBA80 /* BaccaratUITests */,
|
||||
EA5AD1FF2EF34B660040CB90 /* Frameworks */,
|
||||
EAD890B82EF1E9CE006DBA80 /* Products */,
|
||||
EA39BCA72F1D35690073D1E1 /* Recovered References */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@ -296,7 +305,7 @@
|
||||
/* Begin XCBuildConfiguration section */
|
||||
EAD890D62EF1E9CF006DBA80 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = EACONFIG005000000000000 /* Debug.xcconfig */;
|
||||
baseConfigurationReference = EACONFIG005000000000000 /* Baccarat/Configuration/Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
@ -363,7 +372,7 @@
|
||||
};
|
||||
EAD890D72EF1E9CF006DBA80 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = EACONFIG006000000000000 /* Release.xcconfig */;
|
||||
baseConfigurationReference = EACONFIG006000000000000 /* Baccarat/Configuration/Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
@ -445,7 +454,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.7;
|
||||
MARKETING_VERSION = 1.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@ -483,7 +492,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.7;
|
||||
MARKETING_VERSION = 1.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@ -391,8 +391,8 @@ struct SettingsView: View {
|
||||
}
|
||||
.sheet(isPresented: $showPrivacyPolicy) {
|
||||
PrivacyPolicyView(
|
||||
developerName: "Your Name", // TODO: Replace with your name/company
|
||||
contactEmail: "your@email.com" // TODO: Replace with your email
|
||||
developerName: "TopDog Labs",
|
||||
contactEmail: "info@topdoglabs.com"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -433,7 +433,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.7;
|
||||
MARKETING_VERSION = 1.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
@ -466,7 +466,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.7;
|
||||
MARKETING_VERSION = 1.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
|
||||
@ -20,7 +20,7 @@ enum Design {
|
||||
static let showDebugBorders = false
|
||||
|
||||
/// Set to true to show debug log statements
|
||||
static let showDebugLogs = true
|
||||
static let showDebugLogs = false
|
||||
|
||||
/// Debug logger - only prints when showDebugLogs is true
|
||||
static func debugLog(_ message: String) {
|
||||
|
||||
@ -484,8 +484,8 @@ struct SettingsView: View {
|
||||
}
|
||||
.sheet(isPresented: $showPrivacyPolicy) {
|
||||
PrivacyPolicyView(
|
||||
developerName: "Your Name", // TODO: Replace with your name/company
|
||||
contactEmail: "your@email.com" // TODO: Replace with your email
|
||||
developerName: "TopDog Labs",
|
||||
contactEmail: "info@topdoglabs.com"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -615,6 +615,34 @@ public protocol PersistableGameData: Codable, Sendable {
|
||||
- 📢 **Change Notifications** - Callbacks when data changes from other devices
|
||||
- 🔒 **Privacy** - Uses Apple ID, no Game Center required
|
||||
|
||||
### 📈 Analytics
|
||||
|
||||
**AnalyticsService** - Lightweight event tracking with a configurable endpoint.
|
||||
|
||||
```swift
|
||||
let analytics: AnalyticsTracking = AnalyticsService()
|
||||
|
||||
await analytics.track(
|
||||
AnalyticsEvent(
|
||||
name: "round_started",
|
||||
properties: [
|
||||
"game": .string("blackjack"),
|
||||
"tableLimit": .int(25)
|
||||
]
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
**Configure an endpoint later:**
|
||||
```swift
|
||||
await analytics.updateEndpoint(URL(string: "https://example.com/analytics"))
|
||||
```
|
||||
|
||||
**No-op tracker for disabled analytics:**
|
||||
```swift
|
||||
let analytics: AnalyticsTracking = NoOpAnalyticsTracker()
|
||||
```
|
||||
|
||||
### 🎨 Design System
|
||||
|
||||
**CasinoDesign** - Shared design constants.
|
||||
@ -805,6 +833,15 @@ CasinoKit/
|
||||
│ │ └── SessionViews.swift # Session UI components
|
||||
│ ├── Audio/
|
||||
│ │ └── SoundManager.swift
|
||||
│ ├── Analytics/
|
||||
│ │ ├── Models/
|
||||
│ │ │ ├── AnalyticsEvent.swift
|
||||
│ │ │ └── AnalyticsValue.swift
|
||||
│ │ ├── Protocols/
|
||||
│ │ │ └── AnalyticsTracking.swift
|
||||
│ │ └── Services/
|
||||
│ │ ├── AnalyticsService.swift
|
||||
│ │ └── NoOpAnalyticsTracker.swift
|
||||
│ ├── Storage/
|
||||
│ │ └── CloudSyncManager.swift # iCloud persistence
|
||||
│ ├── Theme/
|
||||
@ -886,4 +923,3 @@ private var fontSize: CGFloat = CasinoDesign.BaseFontSize.body
|
||||
## License
|
||||
|
||||
This package is for personal use in your casino game projects.
|
||||
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
//
|
||||
// AnalyticsEvent.swift
|
||||
// CasinoKit
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct AnalyticsEvent: Codable, Equatable, Sendable {
|
||||
public let name: String
|
||||
public let timestamp: Date
|
||||
public let properties: [String: AnalyticsValue]
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
timestamp: Date = Date(),
|
||||
properties: [String: AnalyticsValue] = [:]
|
||||
) {
|
||||
self.name = name
|
||||
self.timestamp = timestamp
|
||||
self.properties = properties
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
//
|
||||
// AnalyticsValue.swift
|
||||
// CasinoKit
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum AnalyticsValue: Codable, Equatable, Sendable {
|
||||
case string(String)
|
||||
case int(Int)
|
||||
case double(Double)
|
||||
case bool(Bool)
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case value
|
||||
}
|
||||
|
||||
private enum ValueType: String, Codable {
|
||||
case string
|
||||
case int
|
||||
case double
|
||||
case bool
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(ValueType.self, forKey: .type)
|
||||
switch type {
|
||||
case .string:
|
||||
self = .string(try container.decode(String.self, forKey: .value))
|
||||
case .int:
|
||||
self = .int(try container.decode(Int.self, forKey: .value))
|
||||
case .double:
|
||||
self = .double(try container.decode(Double.self, forKey: .value))
|
||||
case .bool:
|
||||
self = .bool(try container.decode(Bool.self, forKey: .value))
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
switch self {
|
||||
case let .string(value):
|
||||
try container.encode(ValueType.string, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case let .int(value):
|
||||
try container.encode(ValueType.int, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case let .double(value):
|
||||
try container.encode(ValueType.double, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
case let .bool(value):
|
||||
try container.encode(ValueType.bool, forKey: .type)
|
||||
try container.encode(value, forKey: .value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
//
|
||||
// AnalyticsTracking.swift
|
||||
// CasinoKit
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol AnalyticsTracking: Sendable {
|
||||
func track(_ event: AnalyticsEvent) async
|
||||
func updateEndpoint(_ endpoint: URL?) async
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
//
|
||||
// AnalyticsService.swift
|
||||
// CasinoKit
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public actor AnalyticsService: AnalyticsTracking {
|
||||
private var endpoint: URL?
|
||||
|
||||
public init(endpoint: URL? = nil) {
|
||||
self.endpoint = endpoint
|
||||
}
|
||||
|
||||
public func updateEndpoint(_ endpoint: URL?) async {
|
||||
self.endpoint = endpoint
|
||||
}
|
||||
|
||||
public func track(_ event: AnalyticsEvent) async {
|
||||
guard let endpoint else { return }
|
||||
|
||||
var request = URLRequest(url: endpoint)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let payload = AnalyticsPayload(
|
||||
appIdentifier: AppContext.bundleIdentifier,
|
||||
appVersion: AppContext.appVersion,
|
||||
buildNumber: AppContext.buildNumber,
|
||||
event: event
|
||||
)
|
||||
|
||||
do {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.dateEncodingStrategy = .iso8601
|
||||
request.httpBody = try encoder.encode(payload)
|
||||
_ = try await URLSession.shared.data(for: request)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum AppContext {
|
||||
static var bundleIdentifier: String {
|
||||
Bundle.main.bundleIdentifier ?? "unknown.bundle"
|
||||
}
|
||||
|
||||
static var appVersion: String {
|
||||
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0"
|
||||
}
|
||||
|
||||
static var buildNumber: String {
|
||||
Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "0"
|
||||
}
|
||||
}
|
||||
|
||||
private struct AnalyticsPayload: Codable {
|
||||
let appIdentifier: String
|
||||
let appVersion: String
|
||||
let buildNumber: String
|
||||
let event: AnalyticsEvent
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
//
|
||||
// NoOpAnalyticsTracker.swift
|
||||
// CasinoKit
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct NoOpAnalyticsTracker: AnalyticsTracking {
|
||||
public init() {}
|
||||
|
||||
public func track(_ event: AnalyticsEvent) async {}
|
||||
|
||||
public func updateEndpoint(_ endpoint: URL?) async {}
|
||||
}
|
||||
@ -106,6 +106,13 @@
|
||||
// - CloudSyncManager
|
||||
// - PersistableGameData (protocol)
|
||||
|
||||
// MARK: - Analytics
|
||||
// - AnalyticsTracking (protocol)
|
||||
// - AnalyticsEvent
|
||||
// - AnalyticsValue
|
||||
// - AnalyticsService
|
||||
// - NoOpAnalyticsTracker
|
||||
|
||||
// MARK: - Sessions
|
||||
// - GameSession<Stats> (generic session with game-specific stats)
|
||||
// - GameSpecificStats (protocol for game-specific statistics)
|
||||
@ -137,4 +144,3 @@
|
||||
|
||||
// MARK: - Debug
|
||||
// - debugBorder(_:color:label:) View modifier
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user