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 */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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; };
|
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; };
|
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; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
@ -82,6 +81,15 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup 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 */ = {
|
EA5AD1FF2EF34B660040CB90 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -97,6 +105,7 @@
|
|||||||
EAD890D12EF1E9CF006DBA80 /* BaccaratUITests */,
|
EAD890D12EF1E9CF006DBA80 /* BaccaratUITests */,
|
||||||
EA5AD1FF2EF34B660040CB90 /* Frameworks */,
|
EA5AD1FF2EF34B660040CB90 /* Frameworks */,
|
||||||
EAD890B82EF1E9CE006DBA80 /* Products */,
|
EAD890B82EF1E9CE006DBA80 /* Products */,
|
||||||
|
EA39BCA72F1D35690073D1E1 /* Recovered References */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@ -296,7 +305,7 @@
|
|||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
EAD890D62EF1E9CF006DBA80 /* Debug */ = {
|
EAD890D62EF1E9CF006DBA80 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = EACONFIG005000000000000 /* Debug.xcconfig */;
|
baseConfigurationReference = EACONFIG005000000000000 /* Baccarat/Configuration/Debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
@ -363,7 +372,7 @@
|
|||||||
};
|
};
|
||||||
EAD890D72EF1E9CF006DBA80 /* Release */ = {
|
EAD890D72EF1E9CF006DBA80 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = EACONFIG006000000000000 /* Release.xcconfig */;
|
baseConfigurationReference = EACONFIG006000000000000 /* Baccarat/Configuration/Release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
@ -445,7 +454,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.7;
|
MARKETING_VERSION = 1.9;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -483,7 +492,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.7;
|
MARKETING_VERSION = 1.9;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
@ -391,8 +391,8 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
.sheet(isPresented: $showPrivacyPolicy) {
|
.sheet(isPresented: $showPrivacyPolicy) {
|
||||||
PrivacyPolicyView(
|
PrivacyPolicyView(
|
||||||
developerName: "Your Name", // TODO: Replace with your name/company
|
developerName: "TopDog Labs",
|
||||||
contactEmail: "your@email.com" // TODO: Replace with your email
|
contactEmail: "info@topdoglabs.com"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -433,7 +433,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.7;
|
MARKETING_VERSION = 1.9;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
@ -466,7 +466,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.7;
|
MARKETING_VERSION = 1.9;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_IDENTIFIER)";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
|||||||
@ -20,7 +20,7 @@ enum Design {
|
|||||||
static let showDebugBorders = false
|
static let showDebugBorders = false
|
||||||
|
|
||||||
/// Set to true to show debug log statements
|
/// Set to true to show debug log statements
|
||||||
static let showDebugLogs = true
|
static let showDebugLogs = false
|
||||||
|
|
||||||
/// Debug logger - only prints when showDebugLogs is true
|
/// Debug logger - only prints when showDebugLogs is true
|
||||||
static func debugLog(_ message: String) {
|
static func debugLog(_ message: String) {
|
||||||
|
|||||||
@ -484,8 +484,8 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
.sheet(isPresented: $showPrivacyPolicy) {
|
.sheet(isPresented: $showPrivacyPolicy) {
|
||||||
PrivacyPolicyView(
|
PrivacyPolicyView(
|
||||||
developerName: "Your Name", // TODO: Replace with your name/company
|
developerName: "TopDog Labs",
|
||||||
contactEmail: "your@email.com" // TODO: Replace with your email
|
contactEmail: "info@topdoglabs.com"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -615,6 +615,34 @@ public protocol PersistableGameData: Codable, Sendable {
|
|||||||
- 📢 **Change Notifications** - Callbacks when data changes from other devices
|
- 📢 **Change Notifications** - Callbacks when data changes from other devices
|
||||||
- 🔒 **Privacy** - Uses Apple ID, no Game Center required
|
- 🔒 **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
|
### 🎨 Design System
|
||||||
|
|
||||||
**CasinoDesign** - Shared design constants.
|
**CasinoDesign** - Shared design constants.
|
||||||
@ -805,6 +833,15 @@ CasinoKit/
|
|||||||
│ │ └── SessionViews.swift # Session UI components
|
│ │ └── SessionViews.swift # Session UI components
|
||||||
│ ├── Audio/
|
│ ├── Audio/
|
||||||
│ │ └── SoundManager.swift
|
│ │ └── SoundManager.swift
|
||||||
|
│ ├── Analytics/
|
||||||
|
│ │ ├── Models/
|
||||||
|
│ │ │ ├── AnalyticsEvent.swift
|
||||||
|
│ │ │ └── AnalyticsValue.swift
|
||||||
|
│ │ ├── Protocols/
|
||||||
|
│ │ │ └── AnalyticsTracking.swift
|
||||||
|
│ │ └── Services/
|
||||||
|
│ │ ├── AnalyticsService.swift
|
||||||
|
│ │ └── NoOpAnalyticsTracker.swift
|
||||||
│ ├── Storage/
|
│ ├── Storage/
|
||||||
│ │ └── CloudSyncManager.swift # iCloud persistence
|
│ │ └── CloudSyncManager.swift # iCloud persistence
|
||||||
│ ├── Theme/
|
│ ├── Theme/
|
||||||
@ -886,4 +923,3 @@ private var fontSize: CGFloat = CasinoDesign.BaseFontSize.body
|
|||||||
## License
|
## License
|
||||||
|
|
||||||
This package is for personal use in your casino game projects.
|
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
|
// - CloudSyncManager
|
||||||
// - PersistableGameData (protocol)
|
// - PersistableGameData (protocol)
|
||||||
|
|
||||||
|
// MARK: - Analytics
|
||||||
|
// - AnalyticsTracking (protocol)
|
||||||
|
// - AnalyticsEvent
|
||||||
|
// - AnalyticsValue
|
||||||
|
// - AnalyticsService
|
||||||
|
// - NoOpAnalyticsTracker
|
||||||
|
|
||||||
// MARK: - Sessions
|
// MARK: - Sessions
|
||||||
// - GameSession<Stats> (generic session with game-specific stats)
|
// - GameSession<Stats> (generic session with game-specific stats)
|
||||||
// - GameSpecificStats (protocol for game-specific statistics)
|
// - GameSpecificStats (protocol for game-specific statistics)
|
||||||
@ -137,4 +144,3 @@
|
|||||||
|
|
||||||
// MARK: - Debug
|
// MARK: - Debug
|
||||||
// - debugBorder(_:color:label:) View modifier
|
// - debugBorder(_:color:label:) View modifier
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user