Added demo mode for app review
This commit is contained in:
parent
bbad8d6948
commit
dc17130468
5 changed files with 294 additions and 22 deletions
|
@ -36,14 +36,25 @@ struct CodeUse: Codable, Identifiable, Sendable {
|
||||||
}
|
}
|
||||||
|
|
||||||
// User model
|
// User model
|
||||||
struct PDSUser: Identifiable, Hashable, Sendable {
|
class PDSUser: Identifiable, Hashable, Sendable, ObservableObject {
|
||||||
var id: String // DID
|
let id: String // DID
|
||||||
var handle: String
|
let joinedAt: Date
|
||||||
var displayName: String
|
let avatar: URL?
|
||||||
var description: String
|
|
||||||
var joinedAt: Date
|
@Published var handle: String
|
||||||
var avatar: URL?
|
@Published var displayName: String
|
||||||
var isActive: Bool = true
|
@Published var description: String
|
||||||
|
@Published var isActive: Bool = true
|
||||||
|
|
||||||
|
init(id: String, handle: String, displayName: String, description: String, joinedAt: Date, avatar: URL?, isActive: Bool = true) {
|
||||||
|
self.id = id
|
||||||
|
self.handle = handle
|
||||||
|
self.displayName = displayName
|
||||||
|
self.description = description
|
||||||
|
self.joinedAt = joinedAt
|
||||||
|
self.avatar = avatar
|
||||||
|
self.isActive = isActive
|
||||||
|
}
|
||||||
|
|
||||||
// Add Hashable conformance
|
// Add Hashable conformance
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
|
|
|
@ -6,6 +6,11 @@ import Combine
|
||||||
class PDSViewModel: ObservableObject {
|
class PDSViewModel: ObservableObject {
|
||||||
@Published var pdsService = PDSService()
|
@Published var pdsService = PDSService()
|
||||||
@Published var alertItem: AlertItem?
|
@Published var alertItem: AlertItem?
|
||||||
|
@Published var demoMode: Bool = false
|
||||||
|
|
||||||
|
// Mock data for demo mode
|
||||||
|
private var mockUsers: [PDSUser] = []
|
||||||
|
private var mockInviteCodes: [InviteCode] = []
|
||||||
|
|
||||||
var errorMessage: String? {
|
var errorMessage: String? {
|
||||||
pdsService.errorMessage
|
pdsService.errorMessage
|
||||||
|
@ -17,6 +22,9 @@ class PDSViewModel: ObservableObject {
|
||||||
|
|
||||||
var isAuthenticated: Bool {
|
var isAuthenticated: Bool {
|
||||||
get {
|
get {
|
||||||
|
if demoMode {
|
||||||
|
return true // Always authenticated in demo mode
|
||||||
|
}
|
||||||
let value = pdsService.isAuthenticated
|
let value = pdsService.isAuthenticated
|
||||||
print("PDSViewModel: isAuthenticated getter called, value = \(value)")
|
print("PDSViewModel: isAuthenticated getter called, value = \(value)")
|
||||||
return value
|
return value
|
||||||
|
@ -24,11 +32,17 @@ class PDSViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
var users: [PDSUser] {
|
var users: [PDSUser] {
|
||||||
pdsService.users
|
if demoMode {
|
||||||
|
return mockUsers
|
||||||
|
}
|
||||||
|
return pdsService.users
|
||||||
}
|
}
|
||||||
|
|
||||||
var inviteCodes: [InviteCode] {
|
var inviteCodes: [InviteCode] {
|
||||||
pdsService.inviteCodes
|
if demoMode {
|
||||||
|
return mockInviteCodes
|
||||||
|
}
|
||||||
|
return pdsService.inviteCodes
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add listeners for PDSService changes
|
// Add listeners for PDSService changes
|
||||||
|
@ -48,30 +62,214 @@ class PDSViewModel: ObservableObject {
|
||||||
|
|
||||||
// Method to manually refresh UI data
|
// Method to manually refresh UI data
|
||||||
func refreshUI() {
|
func refreshUI() {
|
||||||
|
print("PDSViewModel: refreshUI called")
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
print("PDSViewModel: sending objectWillChange notification")
|
||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
|
|
||||||
|
if self.demoMode {
|
||||||
|
print("Demo users status: \(self.mockUsers.map { "\($0.handle): \($0.isActive)" }.joined(separator: ", "))")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable demo mode with mock data
|
||||||
|
func enableDemoMode() {
|
||||||
|
demoMode = true
|
||||||
|
createMockData()
|
||||||
|
refreshUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable demo mode
|
||||||
|
func disableDemoMode() {
|
||||||
|
demoMode = false
|
||||||
|
refreshUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create mock data for demo mode
|
||||||
|
private func createMockData() {
|
||||||
|
// Create mock users
|
||||||
|
mockUsers = [
|
||||||
|
PDSUser(
|
||||||
|
id: "did:plc:abcdef123456",
|
||||||
|
handle: "alice",
|
||||||
|
displayName: "Alice Smith",
|
||||||
|
description: "Product designer and developer. Working on decentralized social apps.",
|
||||||
|
joinedAt: Date().addingTimeInterval(-86400 * 30), // 30 days ago
|
||||||
|
avatar: nil,
|
||||||
|
isActive: true
|
||||||
|
),
|
||||||
|
PDSUser(
|
||||||
|
id: "did:plc:ghijkl789012",
|
||||||
|
handle: "bob",
|
||||||
|
displayName: "Bob Johnson",
|
||||||
|
description: "Software engineer. Interested in decentralized protocols and privacy.",
|
||||||
|
joinedAt: Date().addingTimeInterval(-86400 * 15), // 15 days ago
|
||||||
|
avatar: nil,
|
||||||
|
isActive: true
|
||||||
|
),
|
||||||
|
PDSUser(
|
||||||
|
id: "did:plc:mnopqr345678",
|
||||||
|
handle: "charlie",
|
||||||
|
displayName: "Charlie Brown",
|
||||||
|
description: "Community manager and content creator.",
|
||||||
|
joinedAt: Date().addingTimeInterval(-86400 * 5), // 5 days ago
|
||||||
|
avatar: nil,
|
||||||
|
isActive: false
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
// Create mock invite codes
|
||||||
|
mockInviteCodes = [
|
||||||
|
InviteCode(
|
||||||
|
id: "abc-123-xyz",
|
||||||
|
uses: [],
|
||||||
|
createdAt: Date().addingTimeInterval(-86400 * 20), // 20 days ago
|
||||||
|
disabled: false
|
||||||
|
),
|
||||||
|
InviteCode(
|
||||||
|
id: "def-456-uvw",
|
||||||
|
uses: [
|
||||||
|
CodeUse(usedBy: "did:plc:abcdef123456", usedAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-86400 * 10)))
|
||||||
|
],
|
||||||
|
createdAt: Date().addingTimeInterval(-86400 * 25), // 25 days ago
|
||||||
|
disabled: false
|
||||||
|
),
|
||||||
|
InviteCode(
|
||||||
|
id: "ghi-789-rst",
|
||||||
|
uses: [],
|
||||||
|
createdAt: Date().addingTimeInterval(-86400 * 15), // 15 days ago
|
||||||
|
disabled: true
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh invite codes with UI update
|
// Refresh invite codes with UI update
|
||||||
func refreshInviteCodes() async {
|
func refreshInviteCodes() async {
|
||||||
|
if demoMode {
|
||||||
|
// No need to do anything in demo mode, data is already loaded
|
||||||
|
refreshUI()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await pdsService.fetchInviteCodes()
|
await pdsService.fetchInviteCodes()
|
||||||
refreshUI()
|
refreshUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh users with UI update
|
// Refresh users with UI update
|
||||||
func refreshUsers() async {
|
func refreshUsers() async {
|
||||||
|
if demoMode {
|
||||||
|
// No need to do anything in demo mode, data is already loaded
|
||||||
|
refreshUI()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await pdsService.fetchUsers()
|
await pdsService.fetchUsers()
|
||||||
refreshUI()
|
refreshUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable invite code with guaranteed UI update
|
// Disable invite code with guaranteed UI update
|
||||||
func disableInviteCode(_ code: String) async -> Bool {
|
func disableInviteCode(_ code: String) async -> Bool {
|
||||||
|
if demoMode {
|
||||||
|
// In demo mode, just update our mock data
|
||||||
|
if let index = mockInviteCodes.firstIndex(where: { $0.id == code }) {
|
||||||
|
mockInviteCodes[index].disabled = true
|
||||||
|
refreshUI()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
let result = await pdsService.disableInviteCode(code)
|
let result = await pdsService.disableInviteCode(code)
|
||||||
refreshUI()
|
refreshUI()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create invite code in demo mode
|
||||||
|
func createInviteCode() async -> InviteCode? {
|
||||||
|
if demoMode {
|
||||||
|
// Generate a random code in demo mode
|
||||||
|
let randomCode = "demo-\(Int.random(in: 100...999))-\(UUID().uuidString.prefix(4))"
|
||||||
|
let newCode = InviteCode(
|
||||||
|
id: randomCode,
|
||||||
|
uses: [],
|
||||||
|
createdAt: Date(),
|
||||||
|
disabled: false
|
||||||
|
)
|
||||||
|
mockInviteCodes.append(newCode)
|
||||||
|
refreshUI()
|
||||||
|
return newCode
|
||||||
|
}
|
||||||
|
|
||||||
|
let code = await pdsService.createInviteCode()
|
||||||
|
await refreshInviteCodes()
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suspend user in demo mode
|
||||||
|
func suspendUser(userId: String) async -> Bool {
|
||||||
|
if demoMode {
|
||||||
|
print("Suspending user in demo mode: \(userId)")
|
||||||
|
return await withCheckedContinuation { continuation in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let user = self.mockUsers.first(where: { $0.id == userId }) {
|
||||||
|
print("Found user \(user.displayName), setting isActive to false")
|
||||||
|
user.isActive = false
|
||||||
|
self.refreshUI()
|
||||||
|
continuation.resume(returning: true)
|
||||||
|
} else {
|
||||||
|
print("User not found in mockUsers: \(userId)")
|
||||||
|
continuation.resume(returning: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let success = await pdsService.suspendUser(userId: userId, reason: "Demo suspension")
|
||||||
|
await refreshUsers()
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reactivate user in demo mode
|
||||||
|
func reactivateUser(userId: String) async -> Bool {
|
||||||
|
if demoMode {
|
||||||
|
print("Reactivating user in demo mode: \(userId)")
|
||||||
|
return await withCheckedContinuation { continuation in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let user = self.mockUsers.first(where: { $0.id == userId }) {
|
||||||
|
print("Found user \(user.displayName), setting isActive to true")
|
||||||
|
user.isActive = true
|
||||||
|
self.refreshUI()
|
||||||
|
continuation.resume(returning: true)
|
||||||
|
} else {
|
||||||
|
print("User not found in mockUsers: \(userId)")
|
||||||
|
continuation.resume(returning: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let success = await pdsService.reactivateUser(userId: userId)
|
||||||
|
await refreshUsers()
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit user handle in demo mode
|
||||||
|
func editUserHandle(userId: String, newHandle: String) async -> Bool {
|
||||||
|
if demoMode {
|
||||||
|
if let user = mockUsers.first(where: { $0.id == userId }) {
|
||||||
|
user.handle = newHandle
|
||||||
|
refreshUI()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let success = await pdsService.editUserHandle(userId: userId, newHandle: newHandle)
|
||||||
|
await refreshUsers()
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
func login(serverURL: String, username: String, password: String) async {
|
func login(serverURL: String, username: String, password: String) async {
|
||||||
print("PDSViewModel: login called")
|
print("PDSViewModel: login called")
|
||||||
if let credentials = PDSCredentials(serverURL: serverURL, username: username, password: password) {
|
if let credentials = PDSCredentials(serverURL: serverURL, username: username, password: password) {
|
||||||
|
@ -87,7 +285,11 @@ class PDSViewModel: ObservableObject {
|
||||||
|
|
||||||
func logout() {
|
func logout() {
|
||||||
print("PDSViewModel: logout called")
|
print("PDSViewModel: logout called")
|
||||||
pdsService.logout()
|
if demoMode {
|
||||||
|
disableDemoMode()
|
||||||
|
} else {
|
||||||
|
pdsService.logout()
|
||||||
|
}
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,12 +83,22 @@ struct InviteCodesView: View {
|
||||||
}
|
}
|
||||||
.navigationTitle("Invite Codes")
|
.navigationTitle("Invite Codes")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
if viewModel.demoMode {
|
||||||
|
Text("DEMO MODE")
|
||||||
|
.font(.caption)
|
||||||
|
.padding(5)
|
||||||
|
.background(Color.green.opacity(0.2))
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.cornerRadius(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button {
|
Button {
|
||||||
isCreatingCode = true
|
isCreatingCode = true
|
||||||
Task {
|
Task {
|
||||||
await viewModel.pdsService.createInviteCode()
|
await viewModel.createInviteCode()
|
||||||
await viewModel.refreshInviteCodes()
|
|
||||||
isCreatingCode = false
|
isCreatingCode = false
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
@ -64,11 +64,32 @@ struct LoginView: View {
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
}
|
}
|
||||||
|
|
||||||
Button("Toggle Debug Info") {
|
Divider()
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.padding(.horizontal, 40)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
enterDemoMode()
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "rectangle.fill.on.rectangle.fill.circle")
|
||||||
|
.imageScale(.medium)
|
||||||
|
Text("Enter Demo Mode")
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.background(Color.green)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(10)
|
||||||
|
.padding(.horizontal, 40)
|
||||||
|
|
||||||
|
Button("Show Debug Info") {
|
||||||
showingDebugInfo.toggle()
|
showingDebugInfo.toggle()
|
||||||
}
|
}
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.padding(.top, 20)
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.top, 10)
|
||||||
|
|
||||||
if showingDebugInfo {
|
if showingDebugInfo {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
@ -99,6 +120,11 @@ struct LoginView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func enterDemoMode() {
|
||||||
|
debugLogs.append("Entering demo mode...")
|
||||||
|
viewModel.enableDemoMode()
|
||||||
|
}
|
||||||
|
|
||||||
private func login() {
|
private func login() {
|
||||||
isLoggingIn = true
|
isLoggingIn = true
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,18 @@ struct UserListView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Users")
|
.navigationTitle("Users")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
if viewModel.demoMode {
|
||||||
|
Text("DEMO MODE")
|
||||||
|
.font(.caption)
|
||||||
|
.padding(5)
|
||||||
|
.background(Color.green.opacity(0.2))
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.cornerRadius(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.refreshable {
|
.refreshable {
|
||||||
print("⏳ Pull-to-refresh: Fetching users")
|
print("⏳ Pull-to-refresh: Fetching users")
|
||||||
await viewModel.refreshUsers()
|
await viewModel.refreshUsers()
|
||||||
|
@ -93,9 +105,8 @@ struct UserListView: View {
|
||||||
|
|
||||||
Button("Save") {
|
Button("Save") {
|
||||||
Task {
|
Task {
|
||||||
if await viewModel.pdsService.editUserHandle(userId: user.id, newHandle: newHandle) {
|
if await viewModel.editUserHandle(userId: user.id, newHandle: newHandle) {
|
||||||
showingEditSheet = false
|
showingEditSheet = false
|
||||||
await viewModel.refreshUsers() // Refresh to show updated handle
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,9 +123,15 @@ struct UserListView: View {
|
||||||
Button("Cancel", role: .cancel) { }
|
Button("Cancel", role: .cancel) { }
|
||||||
Button("Suspend", role: .destructive) {
|
Button("Suspend", role: .destructive) {
|
||||||
if let user = selectedUser {
|
if let user = selectedUser {
|
||||||
|
print("Attempting to suspend user: \(user.id)")
|
||||||
Task {
|
Task {
|
||||||
await viewModel.pdsService.suspendUser(userId: user.id, reason: suspensionReason)
|
let success = await viewModel.suspendUser(userId: user.id)
|
||||||
await viewModel.refreshUsers() // Refresh after suspension
|
print("Suspend user result: \(success)")
|
||||||
|
|
||||||
|
// Force UI update
|
||||||
|
await MainActor.run {
|
||||||
|
viewModel.refreshUI()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,9 +144,15 @@ struct UserListView: View {
|
||||||
Button("Cancel", role: .cancel) { }
|
Button("Cancel", role: .cancel) { }
|
||||||
Button("Reactivate") {
|
Button("Reactivate") {
|
||||||
if let user = selectedUser {
|
if let user = selectedUser {
|
||||||
|
print("Attempting to reactivate user: \(user.id)")
|
||||||
Task {
|
Task {
|
||||||
await viewModel.pdsService.reactivateUser(userId: user.id)
|
let success = await viewModel.reactivateUser(userId: user.id)
|
||||||
await viewModel.refreshUsers() // Refresh after reactivation
|
print("Reactivate user result: \(success)")
|
||||||
|
|
||||||
|
// Force UI update
|
||||||
|
await MainActor.run {
|
||||||
|
viewModel.refreshUI()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,7 +172,7 @@ struct UserListView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UserRow: View {
|
struct UserRow: View {
|
||||||
let user: PDSUser
|
@ObservedObject var user: PDSUser
|
||||||
|
|
||||||
var formattedDate: String {
|
var formattedDate: String {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
|
|
Loading…
Add table
Reference in a new issue