diff --git a/Flipcash/Core/Controllers/BetaFlags.swift b/Flipcash/Core/Controllers/BetaFlags.swift index 421fab22..850eee51 100644 --- a/Flipcash/Core/Controllers/BetaFlags.swift +++ b/Flipcash/Core/Controllers/BetaFlags.swift @@ -83,11 +83,9 @@ class BetaFlags: ObservableObject { extension BetaFlags { enum Option: String, Hashable, Equatable, Codable, CaseIterable, Identifiable { - + case transactionDetails case vibrateOnScan - case showMissingRendezvous - case enableConfetti case enableCoinbase case coinbaseSandbox @@ -101,10 +99,6 @@ extension BetaFlags { return "Transaction details" case .vibrateOnScan: return "Vibrate on scan" - case .showMissingRendezvous: - return "Show Missing Rendezvous" - case .enableConfetti: - return "Enable Confetti Cannon" case .enableCoinbase: return "Enable Coinbase" case .coinbaseSandbox: @@ -118,10 +112,6 @@ extension BetaFlags { return "If enabled, tapping a transaction in Balance will open a details modal" case .vibrateOnScan: return "If enabled, the device will vibrate to indicate that the camera has registered the code on the bill" - case .showMissingRendezvous: - return "If enabled, pools that have a missing rendezvous key will show a flag in the list" - case .enableConfetti: - return "If enabled, confetti cannon will blast confetti on winning pools" case .enableCoinbase: return "If enabled, Coinbase onramp will be available regardless of region" case .coinbaseSandbox: diff --git a/Flipcash/Core/Controllers/Database/Database+Pools.swift b/Flipcash/Core/Controllers/Database/Database+Pools.swift deleted file mode 100644 index f8b24155..00000000 --- a/Flipcash/Core/Controllers/Database/Database+Pools.swift +++ /dev/null @@ -1,343 +0,0 @@ -// -// Database+Pools.swift -// Code -// -// Created by Dima Bart on 2025-04-11. -// - -import Foundation -import FlipcashCore -import SQLite - -extension Database { - - // MARK: - Get Pool - - - func getPool(poolID: PublicKey) throws -> StoredPool? { - let statement = try reader.prepareRowIterator(""" - SELECT - p.id, - p.fundingAccount, - p.creatorUserID, - p.creationDate, - p.closedDate, - p.isOpen, - p.isHost, - p.name, - p.buyInQuarks, - p.buyInCurrency, - p.resolution, - p.rendezvousSeed, - - p.betsCountYes, - p.betsCountNo, - p.derivationIndex, - p.isFundingDestinationInitialized, - p.userOutcome, - p.userOutcomeQuarks, - p.userOutcomeCurrency - FROM pool p - WHERE p.id = ? - LIMIT 1; - """, bindings: Blob(bytes: poolID.bytes)) - - let t = PoolTable() - - let pools = try statement.map { row in - var rendezvous: KeyPair? - if let seed = row[t.rendezvousSeed] { - rendezvous = KeyPair(seed: seed) - } - - return StoredPool( - id: row[t.id], - fundingAccount: row[t.fundingAccount], - creatorUserID: row[t.creatorUserID], - creationDate: row[t.creationDate], - name: row[t.name], - buyIn: Quarks( - quarks: row[t.buyInQuarks], - currencyCode: row[t.buyInCurrency], - decimals: 6 - ), - - isOpen: row[t.isOpen], - isHost: row[t.isHost], - closedDate: row[t.closedDate], - rendezvous: rendezvous, - resolution: row[t.resolution], - - betCountYes: row[t.betsCountYes], - betCountNo: row[t.betsCountNo], - derivationIndex: row[t.derivationIndex], - isFundingDestinationInitialized: row[t.isFundingDestinationInitialized], - userOutcome: userOutcome(for: row, t: t) - ) - } - - return pools.first - } - - private func userOutcome(for row: RowIterator.Element, t: PoolTable) -> UserOutcome { - let userOutcomeInt = row[t.userOutcome] - - let userOutcome: UserOutcome - if userOutcomeInt != UserOutcome.none.intValue { - let amount = Quarks( - quarks: row[t.userOutcomeQuarks] ?? 0, - currencyCode: row[t.userOutcomeCurrency] ?? .usd, - decimals: 6 - ) - userOutcome = UserOutcome(intValue: userOutcomeInt, amount: amount) - } else { - userOutcome = .none - } - - return userOutcome - } - - func getPools() throws -> [StoredPool] { - let statement = try reader.prepareRowIterator(""" - SELECT - p.id, - p.fundingAccount, - p.creatorUserID, - p.creationDate, - p.closedDate, - p.isOpen, - p.isHost, - p.name, - p.buyInQuarks, - p.buyInCurrency, - p.resolution, - p.rendezvousSeed, - - p.betsCountYes, - p.betsCountNo, - p.derivationIndex, - p.isFundingDestinationInitialized, - p.userOutcome, - p.userOutcomeQuarks, - p.userOutcomeCurrency - FROM pool p - ORDER BY p.closedDate DESC, p.creationDate DESC - LIMIT 1024; - """) - - let t = PoolTable() - - let pools = try statement.map { row in - var rendezvous: KeyPair? - if let seed = row[t.rendezvousSeed] { - rendezvous = KeyPair(seed: seed) - } - - return StoredPool( - id: row[t.id], - fundingAccount: row[t.fundingAccount], - creatorUserID: row[t.creatorUserID], - creationDate: row[t.creationDate], - name: row[t.name], - buyIn: Quarks( - quarks: row[t.buyInQuarks], - currencyCode: row[t.buyInCurrency], - decimals: 6 - ), - - isOpen: row[t.isOpen], - isHost: row[t.isHost], - closedDate: row[t.closedDate], - rendezvous: rendezvous, - resolution: row[t.resolution], - - betCountYes: row[t.betsCountYes], - betCountNo: row[t.betsCountNo], - derivationIndex: row[t.derivationIndex], - isFundingDestinationInitialized: row[t.isFundingDestinationInitialized], - userOutcome: userOutcome(for: row, t: t) - ) - } - - return pools - } - - func getHostedPoolsWithoutRendezvousKeys() throws -> [(PublicKey, Int)] { - let statement = try reader.prepareRowIterator(""" - SELECT - p.id, - p.derivationIndex - FROM - pool p - WHERE - p.isHost AND p.rendezvousSeed IS NULL - LIMIT 1024; - """) - - let t = PoolTable() - - let pools = try statement.map { row in - (row[t.id], row[t.derivationIndex]) - } - - return pools - } - - // MARK: - Insert Pools - - - func insertPool(pool: PoolDescription, rendezvous: KeyPair?, currentUserID: UserID) throws { - let metadata = pool.metadata - let additionalInfo = pool.additionalInfo - - let t = PoolTable() - var setters: [Setter] = [ - t.id <- metadata.id, - t.fundingAccount <- metadata.fundingAccount, - t.creatorUserID <- metadata.creatorUserID, - t.creationDate <- metadata.creationDate, - t.closedDate <- metadata.closedDate, - t.isOpen <- metadata.isOpen, - t.isHost <- currentUserID == metadata.creatorUserID, - t.name <- metadata.name, - t.buyInQuarks <- metadata.buyIn.quarks, - t.buyInCurrency <- metadata.buyIn.currencyCode, - - t.betsCountYes <- additionalInfo.betCountYes, - t.betsCountNo <- additionalInfo.betCountNo, - t.derivationIndex <- additionalInfo.derivationIndex, - t.isFundingDestinationInitialized <- additionalInfo.isFundingDestinationInitialized, - t.userOutcome <- additionalInfo.userOutcome.intValue, - ] - - if let outcomeAmount = additionalInfo.userOutcome.amount { - setters.append(contentsOf: [ - t.userOutcomeQuarks <- outcomeAmount.quarks, - t.userOutcomeCurrency <- outcomeAmount.currencyCode, - ]) - } - - if let resolution = metadata.resolution { - setters.append( - t.resolution <- resolution - ) - } - - if let keyPair = metadata.rendezvous { - setters.append( - t.rendezvousSeed <- keyPair.seed, - ) - - } else if let rendezvous, rendezvous.publicKey == metadata.id { - // When opening a pool from a deeplink, we'll the option to - // provide the rendezvous key directly, but we'll ensure that - // it matches the pool metadata ID first - setters.append( - t.rendezvousSeed <- rendezvous.seed, - ) - } - - try writer.run( - t.table.upsert(setters, onConflictOf: t.id) - ) - } - - func setRendezvousForPool(rendezvous: KeyPair) throws { - let t = PoolTable() - try writer.run( - t.table - .filter(t.id == rendezvous.publicKey) - .update(t.rendezvousSeed <- rendezvous.seed) - ) - } - - // MARK: - Get Bets - - - func betsToDistribute(for poolID: PublicKey, outcome: PoolResoltion) throws -> [StoredBet] { - // Fetch all pool bets filtered by outcome, - // in the event of of a tie, all bets will be - // returned and therefore paid out - let outcomeBets = try getBets( - poolID: poolID, - resolution: outcome - ) - - // If outcome bets is empty that means the - // outcome had no bets and so we'll fetch - // all the bets for the pool, which will - // equivalent to a tie - if outcomeBets.isEmpty { - return try getBets(poolID: poolID) - } - - return outcomeBets - } - - func getBets(poolID: PublicKey, resolution: PoolResoltion? = nil) throws -> [StoredBet] { - var filter = "" - if let resolution, resolution != .refund { - filter = "AND b.selectedOutcome = \(resolution.intValue)" - } - - let statement = try reader.prepareRowIterator(""" - SELECT - b.id, - b.userID, - b.payoutDestination, - b.betDate, - b.selectedOutcome, - b.isFulfilled - FROM - bet b - WHERE b.isFulfilled = 1 AND b.poolID = ? \(filter); - """, bindings: Blob(bytes: poolID.bytes)) - - let t = BetTable() - - let pools = try statement.map { row in - StoredBet( - id: row[t.id], - userID: row[t.userID], - payoutDestination: row[t.payoutDestination], - betDate: row[t.betDate], - selectedOutcome: row[t.selectedOutcome] == 1 ? .yes : .no, - isFulfilled: row[t.isFulfilled] - ) - } - - return pools - } - - // MARK: - Insert Bets - - - func insertBets(poolID: PublicKey, bets: [BetDescription]) throws { - try bets.forEach { - try insertBet(poolID: poolID, bet: $0) - } - } - - func insertBet(poolID: PublicKey, bet: BetDescription) throws { - let metadata = bet.metadata - let t = BetTable() - let setters: [Setter] = [ - t.id <- metadata.id, - t.poolID <- poolID, - t.userID <- metadata.userID, - t.payoutDestination <- metadata.payoutDestination, - t.betDate <- metadata.betDate, - t.selectedOutcome <- metadata.selectedOutcome.intValue, - t.isFulfilled <- bet.isFulfilled, - ] - - try writer.run( - t.table.upsert(setters, onConflictOf: t.id) - ) - } - - func setBetFulfilled(betID: PublicKey) throws { - let t = BetTable() - try writer.run( - t.table - .filter(t.id == betID) - .update(t.isFulfilled <- true) - ) - } -} diff --git a/Flipcash/Core/Controllers/Database/Models/StoredBet.swift b/Flipcash/Core/Controllers/Database/Models/StoredBet.swift deleted file mode 100644 index 59ae7339..00000000 --- a/Flipcash/Core/Controllers/Database/Models/StoredBet.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// StoredBet.swift -// Code -// -// Created by Dima Bart on 2025-07-04. -// - -import Foundation -import FlipcashCore - -struct StoredBet: Identifiable, Sendable, Equatable, Hashable { - let id: PublicKey - let userID: UserID - let payoutDestination: PublicKey - let betDate: Date - let selectedOutcome: PoolResoltion - - let isFulfilled: Bool - - init(id: PublicKey, userID: UserID, payoutDestination: PublicKey, betDate: Date, selectedOutcome: PoolResoltion, isFulfilled: Bool) { - self.id = id - self.userID = userID - self.payoutDestination = payoutDestination - self.betDate = betDate - self.selectedOutcome = selectedOutcome - self.isFulfilled = isFulfilled - } -} diff --git a/Flipcash/Core/Controllers/Database/Models/StoredPool.swift b/Flipcash/Core/Controllers/Database/Models/StoredPool.swift deleted file mode 100644 index 7aa19169..00000000 --- a/Flipcash/Core/Controllers/Database/Models/StoredPool.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// StoredPool.swift -// Code -// -// Created by Dima Bart on 2025-07-04. -// - -import Foundation -import FlipcashCore - -struct StoredPool: Identifiable, Sendable, Equatable, Hashable { - - public let id: PublicKey - public let fundingAccount: PublicKey - public let creatorUserID: UserID - public let creationDate: Date - public let name: String - public let buyIn: Quarks - - public var isOpen: Bool - public var isHost: Bool - public var closedDate: Date? - public var rendezvous: KeyPair? - public var resolution: PoolResoltion? - - public let betCountYes: Int - public let betCountNo: Int - public let derivationIndex: Int - public let isFundingDestinationInitialized: Bool - public let userOutcome: UserOutcome - - var amountInPool: Quarks { - Quarks( - quarks: buyIn.quarks * UInt64(betCountYes + betCountNo), - currencyCode: buyIn.currencyCode, - decimals: 6 - ) - } - - var payout: Quarks? { - if let resolution = resolution { - return payoutFor(resolution: resolution) - } - return nil - } - -// var amountOnYes: Fiat { -// Fiat( -// quarks: buyIn.quarks * UInt64(betCountYes), -// currencyCode: buyIn.currencyCode -// ) -// } -// -// var amountOnNo: Fiat { -// Fiat( -// quarks: buyIn.quarks * UInt64(betCountNo), -// currencyCode: buyIn.currencyCode -// ) -// } - - func payoutFor(resolution: PoolResoltion) -> Quarks { - let winnerCount = winnerCount(for: resolution) - guard winnerCount > 0 else { - return Quarks( - quarks: 0 as UInt64, - currencyCode: buyIn.currencyCode, - decimals: 6 - ) - } - - return Quarks( - quarks: amountInPool.quarks / UInt64(winnerCount), - currencyCode: buyIn.currencyCode, - decimals: 6 - ) - } - - func winnerCount(for resolution: PoolResoltion) -> Int { - let totalBets = betCountYes + betCountNo - - switch resolution { - case .yes: - guard betCountYes > 0 else { - return totalBets - } - - return betCountYes - - case .no: - guard betCountNo > 0 else { - return totalBets - } - - return betCountNo - - case .refund: - return totalBets - } - } -} - -// MARK: - Metadata - - -extension StoredPool { - func metadataToClose(resolution: PoolResoltion?) -> PoolMetadata { - .init( - id: id, - rendezvous: rendezvous, - fundingAccount: fundingAccount, - creatorUserID: creatorUserID, - creationDate: creationDate, - closedDate: closedDate ?? .now, - isOpen: false, - name: name, - buyIn: buyIn, - resolution: resolution - ) - } -} diff --git a/Flipcash/Core/Controllers/Database/Schema.swift b/Flipcash/Core/Controllers/Database/Schema.swift index b46d3f2a..f6d061c2 100644 --- a/Flipcash/Core/Controllers/Database/Schema.swift +++ b/Flipcash/Core/Controllers/Database/Schema.swift @@ -80,45 +80,6 @@ struct CashLinkMetadataTable: Sendable { let canCancel = Expression ("canCancel") } -struct PoolTable: Sendable { - static let name = "pool" - - let table = Table(Self.name) - let id = Expression ("id") - let rendezvousSeed = Expression ("rendezvousSeed") - let fundingAccount = Expression ("fundingAccount") - let creatorUserID = Expression ("creatorUserID") - let creationDate = Expression ("creationDate") - let closedDate = Expression ("closedDate") - let isOpen = Expression ("isOpen") - let isHost = Expression ("isHost") - let name = Expression ("name") - let buyInQuarks = Expression ("buyInQuarks") - let buyInCurrency = Expression ("buyInCurrency") - let resolution = Expression ("resolution") - - let betsCountYes = Expression ("betsCountYes") - let betsCountNo = Expression ("betsCountNo") - let derivationIndex = Expression ("derivationIndex") - let isFundingDestinationInitialized = Expression ("isFundingDestinationInitialized") - let userOutcome = Expression ("userOutcome") - let userOutcomeQuarks = Expression ("userOutcomeQuarks") - let userOutcomeCurrency = Expression ("userOutcomeCurrency") -} - -struct BetTable: Sendable { - static let name = "bet" - - let table = Table(Self.name) - let id = Expression ("id") - let poolID = Expression ("poolID") - let userID = Expression ("userID") - let payoutDestination = Expression ("payoutDestination") - let betDate = Expression ("betDate") - let selectedOutcome = Expression ("selectedOutcome") // 0 = no, 1 = yes, 2+ index of option - let isFulfilled = Expression ("isFulfilled") -} - extension Expression { func alias(_ alias: String) -> Expression { Expression(alias) @@ -138,8 +99,6 @@ extension Database { let mintTable = MintTable() let activityTable = ActivityTable() let cashLinkMetadataTable = CashLinkMetadataTable() - let poolTable = PoolTable() - let betTable = BetTable() try writer.transaction { try writer.run(balanceTable.table.create(ifNotExists: true, withoutRowid: true) { t in @@ -204,50 +163,11 @@ extension Database { t.column(cashLinkMetadataTable.id, primaryKey: true) t.column(cashLinkMetadataTable.vault) t.column(cashLinkMetadataTable.canCancel) - + t.foreignKey(cashLinkMetadataTable.id, references: activityTable.table, activityTable.id, delete: .cascade) }) } - - try writer.transaction { - try writer.run(poolTable.table.create(ifNotExists: true, withoutRowid: true) { t in - t.column(poolTable.id, primaryKey: true) - t.column(poolTable.fundingAccount) - t.column(poolTable.creatorUserID) - t.column(poolTable.creationDate) - t.column(poolTable.closedDate) - t.column(poolTable.isOpen) - t.column(poolTable.isHost) - t.column(poolTable.name) - t.column(poolTable.buyInQuarks) - t.column(poolTable.buyInCurrency) - t.column(poolTable.resolution) - t.column(poolTable.rendezvousSeed) - - t.column(poolTable.betsCountYes) - t.column(poolTable.betsCountNo) - t.column(poolTable.derivationIndex) - t.column(poolTable.isFundingDestinationInitialized) - t.column(poolTable.userOutcome) - t.column(poolTable.userOutcomeQuarks) - t.column(poolTable.userOutcomeCurrency) - }) - } - - try writer.transaction { - try writer.run(betTable.table.create(ifNotExists: true, withoutRowid: true) { t in - t.column(betTable.id, primaryKey: true) - t.column(betTable.poolID) // FK pool.id - t.column(betTable.userID) - t.column(betTable.payoutDestination) - t.column(betTable.betDate) - t.column(betTable.selectedOutcome) - t.column(betTable.isFulfilled, defaultValue: false) - - t.foreignKey(betTable.poolID, references: poolTable.table, poolTable.id, delete: .cascade) - }) - } - + try createIndexesIfNeeded() } @@ -300,16 +220,3 @@ extension CurrencyCode: @retroactive Value { } } -extension PoolResoltion: @retroactive Value { - public static var declaredDatatype: String { - Int64.declaredDatatype - } - - public static func fromDatatypeValue(_ dataValue: Int64) -> PoolResoltion { - PoolResoltion(intValue: Int(dataValue))! - } - - public var datatypeValue: Int64 { - Int64(intValue) - } -} diff --git a/Flipcash/Core/Controllers/Deep Links/Data+PoolLink.swift b/Flipcash/Core/Controllers/Deep Links/Data+PoolLink.swift deleted file mode 100644 index 38245399..00000000 --- a/Flipcash/Core/Controllers/Deep Links/Data+PoolLink.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// Data+PoolLink.swift -// Code -// -// Created by Dima Bart on 2025-07-07. -// - -import Foundation - -extension Data { - static func parseBase64EncodedPoolInfo(_ string: String) throws -> PoolInfo { - let data = Data(base64Encoded: string)! - let info = try JSONDecoder().decode(PoolInfo.self, from: data) - return info - } - - static func base64EncodedPoolInfo(_ info: PoolInfo) throws -> String { - try JSONEncoder().encode(info) - .base64EncodedString() - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "/", with: "_") - } -} - -struct PoolInfo: Codable { - let name: String - let amount: String - let yesCount: Int - let noCount: Int - - enum CodingKeys: String, CodingKey { - case name = "p" - case amount = "a" - case yesCount = "y" - case noCount = "n" - } -} diff --git a/Flipcash/Core/Controllers/Deep Links/DeepLinkController.swift b/Flipcash/Core/Controllers/Deep Links/DeepLinkController.swift index b5aacda4..214afdd0 100644 --- a/Flipcash/Core/Controllers/Deep Links/DeepLinkController.swift +++ b/Flipcash/Core/Controllers/Deep Links/DeepLinkController.swift @@ -73,15 +73,6 @@ final class DeepLinkController { return actionForReceiveRemoteSend(mnemonic: mnemonic) } - case .pool: - if - let rendezvousSeed = route.fragments[.entropy], - let seed = try? Seed32(base58: rendezvousSeed.value) - { - let rendezvous = KeyPair(seed: seed) - return actionForOpenPool(rendezvous: rendezvous) - } - case .verifyEmail: if let code = route.properties["code"], @@ -128,13 +119,6 @@ final class DeepLinkController { ) } - private func actionForOpenPool(rendezvous: KeyPair) -> DeepLinkAction { - DeepLinkAction( - kind: .pool(rendezvous), - sessionAuthenticator: sessionAuthenticator - ) - } - private func actionForVerificationCode(email: String, code: String, clientData: String?) -> DeepLinkAction { DeepLinkAction( kind: .verifyEmail( @@ -214,12 +198,7 @@ struct DeepLinkAction { if case .loggedIn(let container) = sessionAuthenticator.state { container.session.receiveCashLink(mnemonic: mnemonic) } - - case .pool(let rendezvous): - if case .loggedIn(let container) = sessionAuthenticator.state { - container.poolViewModel.openPoolFromDeeplink(rendezvous: rendezvous) - } - + case .verifyEmail(let description): if case .loggedIn(let container) = sessionAuthenticator.state { container.onrampViewModel.confirmEmailFromDeeplinkAction(verification: description) @@ -234,7 +213,6 @@ extension DeepLinkAction { enum Kind { case accessKey(MnemonicPhrase) case receiveCashLink(MnemonicPhrase) - case pool(KeyPair) case verifyEmail(VerificationDescription) } } diff --git a/Flipcash/Core/Controllers/Deep Links/Route.swift b/Flipcash/Core/Controllers/Deep Links/Route.swift index 3b988c2e..d5b0074e 100644 --- a/Flipcash/Core/Controllers/Deep Links/Route.swift +++ b/Flipcash/Core/Controllers/Deep Links/Route.swift @@ -66,10 +66,9 @@ struct Route { extension Route { enum Path { - + case login case cash - case pool case verifyEmail case unknown(String) @@ -90,8 +89,6 @@ extension Route { return .login case "cash", "c": return .cash - case "pool", "p": - return .pool case "verify": return .verifyEmail default: diff --git a/Flipcash/Core/Controllers/PoolController.swift b/Flipcash/Core/Controllers/PoolController.swift deleted file mode 100644 index 73a90797..00000000 --- a/Flipcash/Core/Controllers/PoolController.swift +++ /dev/null @@ -1,397 +0,0 @@ -// -// PoolController.swift -// Code -// -// Created by Dima Bart on 2025-06-20. -// - -import Foundation -import FlipcashCore - -@MainActor -class PoolController: ObservableObject { - - private let keyAccount: KeyAccount - private let owner: AccountCluster - private let userID: UserID - private let client: Client - private let flipClient: FlipClient - private let database: Database - - private let session: Session - private let ratesController: RatesController - - private var ownerKeyPair: KeyPair { - owner.authority.keyPair - } - - // MARK: - Init - - - init(container: Container, session: Session, ratesController: RatesController, keyAccount: KeyAccount, owner: AccountCluster, userID: UserID, database: Database) { - self.keyAccount = keyAccount - self.owner = owner - self.userID = userID - self.client = container.client - self.flipClient = container.flipClient - self.database = database - - self.session = session - self.ratesController = ratesController - - Task { - try await syncPools() - } - } - - // MARK: - Sync - - - func syncPools() async throws { - try await syncPools(since: nil) - try deriveMissingRendezvousKeys() - } - - private func syncPools(since cursorID: ID? = nil) async throws { - let pageSize = 1024 - var cursor: ID? = cursorID - - // 1. Fetch all pool description pages - // that are available on the server - var pools: [PoolDescription] = [] - - var hasMore = true - while hasMore { - let poolDescriptions = try await flipClient.fetchPools( - owner: ownerKeyPair, - pageSize: 1024, - since: cursor - ) - - if !poolDescriptions.isEmpty { - pools.append(contentsOf: poolDescriptions) - cursor = poolDescriptions.last!.cursor - } - - hasMore = poolDescriptions.count == pageSize - } - - // 2. Verify pool metadata and discard any that - // are don't pass validation - pools = pools.filter { _ in - // TODO: Verify signatures for pools and bets - return true - } - - // 3. Store all pools and bets - if !pools.isEmpty { - try database.transaction { - for pool in pools { - try $0.insertPool(pool: pool, rendezvous: nil, currentUserID: userID) - try $0.insertBets(poolID: pool.metadata.id, bets: pool.bets) - } - } - - trace(.success, components: "Inserted \(pools.count) pools") - } else { - trace(.success, components: "No pools") - } - } - - private func deriveMissingRendezvousKeys() throws { - guard let keysAndIndexes = try? database.getHostedPoolsWithoutRendezvousKeys(), !keysAndIndexes.isEmpty else { - print("[PoolController] Rendezvous keys up-to-date") - return - } - - let mnemonic = keyAccount.mnemonic - var keys: [KeyPair] = [] - - keysAndIndexes.forEach { (publicKey, index) in - let account = PoolAccount(mnemonic: mnemonic, index: index) - if account.rendezvous.publicKey == publicKey { - keys.append(account.rendezvous) - } - } - - if !keys.isEmpty { - print("[PoolController] Derived \(keys.count) missing rendezvous keys") - try assignRendezvousKeys(keys: keys) - } - } - -// private func findRendezvousKeysIfNeeded() { -// guard let keysToFind = try? database.getHostedPoolsWithoutRendezvousKeys(hostID: userID) else { -// return -// } -// -// Task.detached(priority: .background) { [weak self] in -// guard let self = self else { return } -// -// let keys = findRendezvousKeys(for: keysToFind) -// try await assignRendezvousKeys(keys: keys) -// } -// } -// -// nonisolated -// private func findRendezvousKeys(for ids: [PublicKey]) -> [PublicKey: KeyPair] { -// let start = Date.now -// -// // The public keys of all the -// // rendezvous keys we need to find -// var rendezvousToFind = Set(ids) -// var foundKeyPairs: [PublicKey: KeyPair] = [:] -// -// let mnemonic = keyAccount.mnemonic -// for i in 0...2048 { -// let account = PoolAccount(mnemonic: mnemonic, index: i) -// let accountID = account.rendezvous.publicKey -// -// if rendezvousToFind.remove(accountID) != nil { -// foundKeyPairs[accountID] = account.rendezvous -// } -// -// if rendezvousToFind.isEmpty { -// break -// } -// } -// -// print("[PoolController] findRendezvousKeys took: \(Date.now.formattedMilliseconds(from: start)) ") -// return foundKeyPairs -// } - - private func assignRendezvousKeys(keys: [KeyPair]) throws { - try database.transaction { - for rendezvous in keys { - try $0.setRendezvousForPool(rendezvous: rendezvous) - } - } - } - - // MARK: - Pools - - - func updatePool(poolID: PublicKey, rendezvous: KeyPair? = nil) async throws { - let pool = try await flipClient.fetchPool(poolID: poolID, owner: ownerKeyPair) - - try database.transaction { - try $0.insertPool(pool: pool, rendezvous: rendezvous, currentUserID: userID) - try $0.insertBets(poolID: pool.metadata.id, bets: pool.bets) - } - } - - func createPool(name: String, buyIn: Quarks) async throws -> PublicKey { - let info = try await client.fetchAccountInfo( - type: .primary, - owner: ownerKeyPair - ) - - guard let nextIndex = info.nextPoolIndex else { - throw Error.nextPoolIndexNotFound - } - - let poolAccount = PoolAccount( - mnemonic: keyAccount.mnemonic, - index: nextIndex - ) - - let metadata = PoolMetadata( - id: poolAccount.rendezvous.publicKey, - rendezvous: poolAccount.rendezvous, - fundingAccount: poolAccount.cluster.vaultPublicKey, - creatorUserID: userID, - creationDate: .now, - closedDate: nil, - isOpen: true, - name: name, - buyIn: buyIn, - resolution: nil - ) - - // Create the blockchain accounts - // for this pool cluster - try await client.createAccounts( - owner: ownerKeyPair, - mint: .usdc, - cluster: poolAccount.cluster, - kind: .pool, - derivationIndex: poolAccount.index - ) - - // Create pool metadata - try await flipClient.createPool( - poolMetadata: metadata, - owner: ownerKeyPair - ) - - try await updatePool( - poolID: metadata.id, - rendezvous: metadata.rendezvous - ) - - return metadata.id - } - - @discardableResult - func createBet(pool: StoredPool, outcome: PoolResoltion) async throws -> BetMetadata { - let poolID = pool.id - - // Bet IDs are always deterministically derived - // so any subsequent payment attempts use the - // same betID (ie. retries, etc) - let betKeyPair = KeyPair.deriveBetID( - poolID: poolID, - userID: userID - ) - - let betID = betKeyPair.publicKey - - // 1. Create the bet on the server before - // the payment is made - let metadata = BetMetadata( - id: betID, - userID: userID, - payoutDestination: owner.vaultPublicKey, - betDate: .now, - selectedOutcome: outcome - ) - - guard let rendezvous = pool.rendezvous else { - throw Error.poolRendezvousMissing - } - - try await flipClient.createBet( - poolRendezvous: rendezvous, - betMetadata: metadata, - owner: ownerKeyPair - ) - - // 2. Get the current conversion rate - // and pay for the bet buyIn - let exchangedFiat = try ratesController.exchangedFiat(for: pool.buyIn) - - // 3. Pay for the bet. Any failure here can - // be retried with the existing bet ID - try await client.transfer( - exchangedFiat: exchangedFiat, - owner: owner, - destination: pool.fundingAccount, - rendezvous: betID // NOT the pool rendezvous, it's the intentID - ) - - try await updatePool(poolID: poolID) - - session.updatePostTransaction() - - return metadata - } - - func declareOutcome(pool: StoredPool, outcome: PoolResoltion) async throws { - var closingMetadata = pool.metadataToClose(resolution: nil) - if pool.isOpen { - // First, close voting on the pool - try await flipClient.closePool( - poolMetadata: closingMetadata, - owner: ownerKeyPair - ) - } - - closingMetadata.resolution = outcome - - // Declare the pool outcome - try await flipClient.resolvePool( - poolMetadata: closingMetadata, - owner: ownerKeyPair - ) - - // After the pool is closed, we'll need to - // ensure that we have the most up-to-date - // bets, otherwise the distribution will fail - try await updatePool(poolID: pool.id) - - let poolAccount = PoolAccount( - mnemonic: keyAccount.mnemonic, - index: pool.derivationIndex - ) - - // Determine which bets need to be paid out - let distributionBets = try database.betsToDistribute( - for: pool.id, - outcome: outcome - ) - - if distributionBets.count > 0 { - - // Obtain the latest pool balance; we can't - // rely on the exchange rates so we have to - // divide the existing balance in quarks - let poolBalance = try await client.fetchLinkedAccountBalance( - owner: ownerKeyPair, - account: poolAccount.cluster.vaultPublicKey - ) - - // Calculate all the distribution amounts - // based on the bets that were placed and - // need to be paid out - let distributions = distributionBets.distributePool(balance: poolBalance) - - // Distribute the winnings to all betting - // accounts in the pool - try await client.distributePoolWinnings( - source: poolAccount.cluster, - distributions: distributions, - owner: ownerKeyPair - ) - - session.updatePostTransaction() - - trace(.success, components: "Distributions: \n\(distributions.map { "\($0.amount.quarks.formatted()): \($0.destination.base58)" }.joined(separator: "\n"))") - } else { - trace(.success, components: "No distributions") - } - } -} - -extension Array where Element == StoredBet { - func distributePool(balance: Quarks) -> [PoolDistribution] { - // Calculate distributions based on the total pool balance - // and the number of winning bets to pay out - let count = UInt64(self.count) - let distributionQuarks = balance.quarks / count - let remainderQuarks = balance.quarks % count - - let distributions: [PoolDistribution] = self.enumerated().map { index, bet in - .init( - destination: bet.payoutDestination, - amount: Quarks( - quarks: distributionQuarks + (index < remainderQuarks ? 1 : 0), - currencyCode: balance.currencyCode, - decimals: 6 - ) - ) - } - - return distributions - } -} - -// MARK: - Errors - - -extension PoolController { - enum Error: Swift.Error { - case nextPoolIndexNotFound - case exchangeRateUnavailable - case poolRendezvousMissing - } -} - -// MARK: - Mock - - -extension PoolController { - static let mock = PoolController( - container: .mock, - session: .mock, - ratesController: .mock, - keyAccount: .mock, - owner: .mock, - userID: UUID(), - database: .mock - ) -} diff --git a/Flipcash/Core/Screens/Main/ScanScreen.swift b/Flipcash/Core/Screens/Main/ScanScreen.swift index 87d6fa92..2a9c7244 100644 --- a/Flipcash/Core/Screens/Main/ScanScreen.swift +++ b/Flipcash/Core/Screens/Main/ScanScreen.swift @@ -16,7 +16,6 @@ struct ScanScreen: View { @EnvironmentObject private var betaFlags: BetaFlags @ObservedObject private var session: Session - @ObservedObject private var poolViewModel: PoolViewModel @ObservedObject private var onrampViewModel: OnrampViewModel @StateObject private var viewModel: ScanViewModel @@ -66,7 +65,6 @@ struct ScanScreen: View { self.container = container self.sessionContainer = sessionContainer self.session = sessionContainer.session - self.poolViewModel = sessionContainer.poolViewModel self.onrampViewModel = sessionContainer.onrampViewModel _viewModel = .init( diff --git a/Flipcash/Core/Screens/Pools/EnterPoolAmountScreen.swift b/Flipcash/Core/Screens/Pools/EnterPoolAmountScreen.swift deleted file mode 100644 index 5f3208b2..00000000 --- a/Flipcash/Core/Screens/Pools/EnterPoolAmountScreen.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// EnterPoolAmountScreen.swift -// Code -// -// Created by Dima Bart on 2025-06-18. -// - -import SwiftUI -import FlipcashUI -import FlipcashCore - -struct EnterPoolAmountScreen: View { - - @EnvironmentObject private var ratesController: RatesController - - @ObservedObject private var viewModel: PoolViewModel - - @State private var isShowingCurrencySelection: Bool = false - - // MARK: - Init - - - init(viewModel: PoolViewModel) { - self.viewModel = viewModel - } - - // MARK: - Body - - - var body: some View { - Background(color: .backgroundMain) { - EnterAmountView( - mode: .currency, - enteredAmount: $viewModel.enteredPoolAmount, - subtitle: .singleTransactionLimit, - actionState: .constant(.normal), - actionEnabled: { _ in - viewModel.enteredPoolFiat != nil && (viewModel.enteredPoolFiat?.underlying.quarks ?? 0) > 0 - }, - action: viewModel.submitPoolAmountAction, - currencySelectionAction: showCurrencySelection - ) - .foregroundColor(.textMain) - .padding(20) - .sheet(isPresented: $isShowingCurrencySelection) { - CurrencySelectionScreen( - isPresented: $isShowingCurrencySelection, - kind: .entry, - ratesController: ratesController - ) - } - } - .navigationTitle("Cost to Join") - .navigationBarTitleDisplayMode(.inline) - .dialog(item: $viewModel.dialogItem) - } - - // MARK: - Actions - - - private func showCurrencySelection() { - isShowingCurrencySelection.toggle() - } -} diff --git a/Flipcash/Core/Screens/Pools/EnterPoolNameScreen.swift b/Flipcash/Core/Screens/Pools/EnterPoolNameScreen.swift deleted file mode 100644 index c4e522dd..00000000 --- a/Flipcash/Core/Screens/Pools/EnterPoolNameScreen.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// EnterPoolNameScreen.swift -// Code -// -// Created by Dima Bart on 2025-06-18. -// - -import SwiftUI -import FlipcashUI -import FlipcashCore - -struct EnterPoolNameScreen: View { - - @Binding var isPresented: Bool - - @ObservedObject private var viewModel: PoolViewModel - - @FocusState private var isFocused: Bool - - @State private var didShowKeyboard: Bool = false - - // MARK: - Init - - - init(isPresented: Binding, viewModel: PoolViewModel) { - self._isPresented = isPresented - self.viewModel = viewModel - } - - // MARK: - Body - - - var body: some View { - NavigationStack(path: $viewModel.createPoolPath) { - Background(color: .backgroundMain) { - VStack(alignment: .center, spacing: 0) { - - Spacer() - - TextField("", text: $viewModel.enteredPoolName, prompt: Text("Question"), axis: .vertical) - .lineLimit(1...) - .focused($isFocused) - .font(.appDisplaySmall) - .foregroundStyle(Color.textMain) - .multilineTextAlignment(.center) - .truncationMode(.head) - .textInputAutocapitalization(.sentences) - .padding([.leading, .trailing], 0) - - Spacer() - - VStack(spacing: 10) { - Text("Pose a Yes or No question") - .font(.appTextMedium) - .foregroundStyle(Color.textSecondary) - -// Text("\"Is Johnny going to hit a home run?\"") -// .font(.appTextSmall) -// .foregroundStyle(Color.textSecondary) - } - .multilineTextAlignment(.center) - - Spacer() - - CodeButton( - style: .filled, - title: "Next", - disabled: !viewModel.isEnteredPoolNameValid - ) { - hideKeyboard() - viewModel.submitPoolNameAction() - } - } - .foregroundColor(.textMain) - .frame(maxHeight: .infinity) - .padding(20) - } - .onAppear(perform: onAppear) - .navigationTitle("Pose a Question") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - ToolbarCloseButton(binding: $isPresented) - } - } - .navigationDestination(for: CreatePoolPath.self) { path in - switch path { - case .enterPoolAmount: - EnterPoolAmountScreen(viewModel: viewModel) - case .poolSummary: - PoolSummaryScreen(viewModel: viewModel) - } - } - } - } - - private func onAppear() { - if !didShowKeyboard { - didShowKeyboard = true - showKeyboard() - } - } - - private func showKeyboard() { - isFocused = true - } - - private func hideKeyboard() { - isFocused = false - } -} diff --git a/Flipcash/Core/Screens/Pools/PoolDetailsScreen.swift b/Flipcash/Core/Screens/Pools/PoolDetailsScreen.swift deleted file mode 100644 index 09059cbc..00000000 --- a/Flipcash/Core/Screens/Pools/PoolDetailsScreen.swift +++ /dev/null @@ -1,527 +0,0 @@ -// -// PoolDetailsScreen.swift -// Code -// -// Created by Dima Bart on 2025-06-18. -// - -import SwiftUI -import FlipcashUI -import FlipcashCore - -struct PoolDetailsScreen: View { - - @ObservedObject private var viewModel: PoolViewModel - - @StateObject private var updateablePool: Updateable - @StateObject private var updateableBets: Updateable<[StoredBet]> - @StateObject private var poller: Poller - - @State private var showingDeclareOutcome: PoolResoltion? - - @State private var localDialogItem: DialogItem? - - @State private var confettiTrigger: Int = 0 - - private let userID: UserID - private let poolID: PublicKey - private let database: Database - - private var pool: StoredPool? { - updateablePool.value - } - - private var bets: [StoredBet] { - updateableBets.value - } - - private var hasResolution: Bool { - pool?.resolution != nil - } - - private var userBet: StoredBet? { - // Disregard existing user bets that - // are not fulfilled. Subsequent bet - // will replace existing ones - bets.filter { $0.isFulfilled && $0.userID == userID }.first - } - - private var hasUserBet: Bool { - userBet != nil - } - - private var countOnYes: Int { - pool?.betCountYes ?? 0 - } - - private var countOnNo: Int { - pool?.betCountNo ?? 0 - } - - private var isWinner: Bool { - guard let resolution = pool?.resolution else { - return false - } - - switch resolution { - case .yes: - return userBet?.selectedOutcome == .yes - case .no: - return userBet?.selectedOutcome == .no - case .refund: - return false - } - } - -// private var amountOnYes: Fiat { -// pool?.amountOnYes ?? 0 -// } -// -// private var amountOnNo: Fiat { -// pool?.amountOnNo ?? 0 -// } - - private var amountInPool: Quarks { - pool?.amountInPool ?? 0 - } - - private var buyIn: Quarks { - pool?.buyIn ?? 0 - } - - private var stateForYes: VoteButton.State { - if let resoltion = pool?.resolution { - if resoltion == .yes { - .winner - } else { - .normal - } - } else if let userBet, userBet.selectedOutcome == .yes { - .selected - } else { - .normal - } - } - - private var stateForNo: VoteButton.State { - if let resoltion = pool?.resolution { - if resoltion == .no { - .winner - } else { - .normal - } - } else if let userBet, userBet.selectedOutcome == .no { - .selected - } else { - .normal - } - } - - // MARK: - Init - - - init(userID: UserID, poolID: PublicKey, database: Database, viewModel: PoolViewModel) { - self.userID = userID - self.poolID = poolID - self.database = database - self.viewModel = viewModel - - _updateablePool = .init(wrappedValue: Updateable { - try? database.getPool(poolID: poolID) - }) - - _updateableBets = .init(wrappedValue: Updateable { - (try? database.getBets(poolID: poolID)) ?? [] - }) - - _poller = .init(wrappedValue: Poller(seconds: 10, fireImmediately: true) { [weak viewModel] in - Task { @MainActor in - viewModel?.updatePool(poolID: poolID, rendezvous: nil) - } - }) - } - - // MARK: - Body - - - var body: some View { - Background(color: .backgroundMain) { - if let pool { - poolDetails(pool: pool) - } else { - VStack { - LoadingView(color: .white) - } - } - } - .navigationTitle("") - .navigationBarTitleDisplayMode(.inline) - .dialog(item: $localDialogItem) - .dialog(item: $viewModel.dialogItem) - } - - @ViewBuilder private func poolDetails(pool: StoredPool) -> some View { - VStack { - Spacer() - - Text(pool.name) - .font(.appTextXL) - .foregroundStyle(Color.textMain) - - Spacer() - - VStack(spacing: 10) { - AmountText( - flagStyle: amountInPool.currencyCode.flagStyle, - content: amountInPool.formatted(), - showChevron: false, - canScale: false - ) - .font(.appDisplayMedium) - .foregroundStyle(Color.textMain) - - Text(hasResolution ? "was in pool" : "in pool so far") - .font(.appTextLarge) - .foregroundStyle(Color.textSecondary) - } - .padding(.horizontal, 20) - .padding(.vertical, 30) - - Spacer() - - VStack(spacing: 0) { - HStack { - VStack(spacing: 0) { - VoteButton( - state: stateForYes, - name: "Yes", - count: countOnYes, - ) { - viewModel.selectBetAction(outcome: .yes, for: pool) - } - .disabled(hasUserBet) - .zIndex(1) - - YouVotedBadge() - .offset(y: -Metrics.boxRadius) - .zIndex(0) - .opacity(userBet?.selectedOutcome == .yes ? 1 : 0) - } - - VStack(spacing: 0) { - VoteButton( - state: stateForNo, - name: "No", - count: countOnNo - ) { - viewModel.selectBetAction(outcome: .no, for: pool) - } - .disabled(hasUserBet) - .zIndex(1) - - YouVotedBadge() - .offset(y: -Metrics.boxRadius) - .zIndex(0) - .opacity(userBet?.selectedOutcome == .no ? 1 : 0) - } - } - .background { - ConfettiBox(trigger: $confettiTrigger) - } - .onAppear { - if BetaFlags.shared.hasEnabled(.enableConfetti) && isWinner { - Task { - try await Task.delay(milliseconds: 500) - confettiTrigger += 1 - } - } - } - } - - Spacer() - - if !hasResolution && !hasUserBet { - VStack { - Text("Tap Yes or No to buy in for ") - .font(.appTextSmall) - .foregroundStyle(Color.textSecondary) - + - Text(buyIn.formatted()) - .font(.appTextSmall) - .foregroundStyle(Color.textMain) - -// AmountText( -// flagStyle: buyIn.currencyCode.flagStyle, -// flagSize: .small, -// content: buyIn.formatted(), -// showChevron: false, -// canScale: false -// ) -// .font(.appTextMedium) -// .foregroundStyle(Color.textMain) - } - .padding(.top, -40) // Offset for YouVotedBadge - } - - Spacer() - - if let resolution = pool.resolution { - bottomViewForResoultion(pool: pool, resolution: resolution) - } else { - if pool.isHost { - bottomViewForHost() - } else { - bottomView(pool: pool) - } - } - } - .multilineTextAlignment(.center) - .padding(.horizontal, 20) - .padding(.bottom, 20) - .sheet(item: $viewModel.isShowingBetConfirmation) { outcome in - PartialSheet { - ModalSwipeToBet( - fiat: pool.buyIn, - subtext: outcome == .yes ? "for Yes" : "for No", - swipeText: "Swipe To Pay", - cancelTitle: "Cancel" - ) { - try await viewModel.betAction( - pool: pool, - outcome: outcome - ) - - } dismissAction: { - viewModel.isShowingBetConfirmation = nil - } cancelAction: { - viewModel.isShowingBetConfirmation = nil - } - } - } - .sheet(item: $showingDeclareOutcome) { outcome in - PartialSheet { - ModalSwipeToDeclareWinner( - outcome: outcome, - amount: pool.payoutFor(resolution: outcome), - swipeText: "Swipe To Confirm", - cancelTitle: "Cancel" - ) { - try await viewModel.declarOutcomeAction( - pool: pool, - outcome: outcome - ) - - } dismissAction: { - showingDeclareOutcome = nil - } cancelAction: { - showingDeclareOutcome = nil - } - } - } - } - - @ViewBuilder private func bottomViewForResoultion(pool: StoredPool, resolution: PoolResoltion) -> some View { - VStack { - if resolution == .refund { - Text("Tie") - .font(.appDisplaySmall) - .foregroundStyle(Color.textMain) - } - - let winnerCount = pool.winnerCount(for: resolution) - let payout = pool.payoutFor(resolution: resolution) - Group { - switch resolution { - case .yes, .no: - if winnerCount == 1 { - Text("The winner received \(payout.formatted())") - } else { - Text("Each winner received \(payout.formatted())") - } - case .refund: - Text("Everyone got their money back") - } - } - .font(.appTextMedium) - .foregroundStyle(Color.textSecondary) - } - - Spacer() - } - - @ViewBuilder private func bottomView(pool: StoredPool) -> some View { - VStack(spacing: 25) { - Text("The person who created the pool gets to decide the outcome of the pool in their sole discretion") - .font(.appTextSmall) - .foregroundStyle(Color.textSecondary) - .multilineTextAlignment(.center) - - // Pool rendezvous is required - // to create the share link - if pool.rendezvous != nil { - CodeButton( - style: .filled, - title: "Share Pool With Friends", - action: sharePoolAction - ) - } - } - } - - @ViewBuilder private func bottomViewForHost() -> some View { - VStack(spacing: 0) { - CodeButton( - style: .filled, - title: "Share Pool With Friends", - action: sharePoolAction - ) - - CodeButton( - style: .subtle, - title: "Declare the Outcome" - ) { - localDialogItem = .init( - style: .standard, - title: "What was the winning outcome?", - subtitle: nil, - dismissable: true, - actions: { - .standard("Yes") { - showingDeclareOutcome = .yes - }; - .standard("No") { - showingDeclareOutcome = .no - }; - .outline("Tie (Refund Everyone)") { - showingDeclareOutcome = .refund - }; - .cancel() - } - ) - } - } - .padding(.bottom, -20) - } - - // MARK: - Actions - - - private func sharePoolAction() { - guard let pool, let rendezvous = pool.rendezvous else { - return - } - - let info = PoolInfo( - name: pool.name, - amount: pool.buyIn.formatted(), - yesCount: pool.betCountYes, - noCount: pool.betCountNo - ) - - print("\(info)") - ShareSheet.present(url: .poolLink(rendezvous: rendezvous, info: info)) - } -} - -// MARK: - YouVotedBadge - - -private struct YouVotedBadge: View { - var body: some View { - VStack { - Text("You said") - .font(.appTextSmall) - .offset(y: Metrics.boxRadius * 0.4) - } - .foregroundStyle(.white.opacity(0.6)) - .frame(width: 90, height: 45) - .background( - RoundedRectangle(cornerRadius: Metrics.boxRadius) - .fill(Color.extraLightFill) - .strokeBorder(Color.lightStroke, lineWidth: Metrics.inputFieldBorderWidth(highlighted: false)) - ) - } -} - -// MARK: - Colors - - -extension Color { - static let extraLightFill = Color(r: 12, g: 37, b: 24) - static let winnerGreen = Color(r: 67, g: 144, b: 84) - static let lightStroke = Color.textSecondary.opacity(0.15) -} - -// MARK: - VoteButton - - -private struct VoteButton: View { - - let state: State - let name: String - let count: Int - let action: () -> Void - - private var strokeColor: Color { - switch state { - case .normal: - .lightStroke - case .selected: - .clear - case .winner: - .clear - } - } - - private var fillColor: Color { - switch state { - case .normal: - .extraLightFill - case .selected: - .white - case .winner: - .winnerGreen - } - } - - private var textColor: Color { - switch state { - case .normal: - .white.opacity(0.6) - case .selected: - .backgroundMain - case .winner: - .white - } - } - - init(state: State, name: String, count: Int, action: @escaping () -> Void) { - self.state = state - self.name = name - self.count = count - self.action = action - } - - var body: some View { - Button { - action() - } label: { - VStack { - Text(name) - .font(.appDisplaySmall) - Text("\(count) \(count == 1 ? "person" : "people")") - .font(.appTextSmall) - } - .foregroundStyle(textColor) - .frame(height: 150) - .frame(maxWidth: .infinity) - .background( - RoundedRectangle(cornerRadius: Metrics.boxRadius) - .fill(fillColor) - .strokeBorder(strokeColor, lineWidth: 1) - ) - } - } -} - -extension VoteButton { - enum State { - case normal - case selected - case winner - } -} diff --git a/Flipcash/Core/Screens/Pools/PoolSummaryScreen.swift b/Flipcash/Core/Screens/Pools/PoolSummaryScreen.swift deleted file mode 100644 index 662a232a..00000000 --- a/Flipcash/Core/Screens/Pools/PoolSummaryScreen.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// PoolSummaryScreen.swift -// Code -// -// Created by Dima Bart on 2025-06-18. -// - -import SwiftUI -import FlipcashUI -import FlipcashCore - -struct PoolSummaryScreen: View { - - @ObservedObject private var viewModel: PoolViewModel - - // MARK: - Init - - - init(viewModel: PoolViewModel) { - self.viewModel = viewModel - } - - // MARK: - Body - - - var body: some View { - Background(color: .backgroundMain) { - VStack { - Spacer() - - VStack(spacing: 40) { - Text(viewModel.enteredPoolNameSantized) - .font(.appTextXL) - .foregroundStyle(Color.textMain) - - if let poolBuyIn = viewModel.enteredPoolFiat?.converted { - BorderedContainer { - VStack(spacing: 10) { - AmountText( - flagStyle: poolBuyIn.currencyCode.flagStyle, - content: poolBuyIn.formatted(), - showChevron: false, - canScale: false - ) - .font(.appDisplayMedium) - .foregroundStyle(Color.textMain) - - Text("to join") - .font(.appTextMedium) - .foregroundStyle(Color.textSecondary) - } - .padding(.horizontal, 20) - .padding(.vertical, 30) - } - } - } - .padding(.horizontal, 20) - - Spacer() - - VStack(spacing: 25) { - Text("As the pool host, you will decide the outcome of the pool in your sole discretion") - .font(.appTextSmall) - .foregroundStyle(Color.textSecondary) - .multilineTextAlignment(.center) - - CodeButton( - state: viewModel.createPoolButtonState, - style: .filled, - title: "Create Your Pool", - disabled: !viewModel.canCreatePool, - action: viewModel.createPoolAction - ) - } - } - .multilineTextAlignment(.center) - .padding(20) - } - .navigationTitle("Create Pool") - .navigationBarTitleDisplayMode(.inline) - } -} diff --git a/Flipcash/Core/Screens/Pools/PoolVault.swift b/Flipcash/Core/Screens/Pools/PoolVault.swift deleted file mode 100644 index 0d271502..00000000 --- a/Flipcash/Core/Screens/Pools/PoolVault.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// PoolVault.swift -// Code -// -// Created by Dima Bart on 2023-05-05. -// - -import Foundation -import FlipcashCore - -//@MainActor -//class PoolVault { -// -// func poolAccounts() -> [String: PoolAccount]? { -// Keychain.poolAccounts -// } -// -// func poolAccount(for id: PublicKey) -> PoolAccount? { -// Keychain.poolAccounts?[id.base58] -// } -// -// func insert(_ poolAccount: PoolAccount) { -// let key = poolAccount.keychainKey.base58 -// -// if var poolAccounts = Keychain.poolAccounts { -// poolAccounts[key] = poolAccount -// Keychain.poolAccounts = poolAccounts -// } else { -// Keychain.poolAccounts = [key: poolAccount] -// } -// } -// -// func remove(_ poolAccount: PoolAccount) { -// remove(poolAccount.keychainKey) -// } -// -// func remove(_ poolID: PublicKey) { -// Keychain.poolAccounts?[poolID.base58] = nil -// } -// -// func nuke() { -// Keychain.poolAccounts = nil -// } -//} -// -//private extension PoolAccount { -// var keychainKey: PublicKey { -// rendezvous.publicKey -// } -//} -// -//extension PoolVault { -// static func prettyPrinted() { -// if let keys = Keychain.poolAccounts?.keys { -// print("Pool Vault Accounts (\(keys.count)):") -// print(" - \(keys.joined(separator: "\n"))") -// } -// } -//} -// -//// MARK: - Keychain - -// -//private extension Keychain { -// -// @SecureCodable(.poolAccounts, sync: true) -// static var poolAccounts: [String: PoolAccount]? // Keyed by vault public key -//} diff --git a/Flipcash/Core/Screens/Pools/PoolViewModel.swift b/Flipcash/Core/Screens/Pools/PoolViewModel.swift deleted file mode 100644 index 69fcc3e3..00000000 --- a/Flipcash/Core/Screens/Pools/PoolViewModel.swift +++ /dev/null @@ -1,306 +0,0 @@ -// -// PoolViewModel.swift -// Code -// -// Created by Dima Bart on 2025-06-18. -// - -import SwiftUI -import FlipcashUI -import FlipcashCore -import StoreKit - -@MainActor -class PoolViewModel: ObservableObject { - - @Published var createPoolPath: [CreatePoolPath] = [] - - @Published var poolListPath: [PoolListPath] = [] - - @Published var enteredPoolName: String = "" - - @Published var enteredPoolAmount: String = "" - - @Published var createPoolButtonState: ButtonState = .normal - - @Published var isShowingCreatePoolFlow: Bool = false - - @Published var isShowingBetConfirmation: PoolResoltion? - - @Published var isShowingPoolList: Bool = false { - didSet { - if !isShowingPoolList { - poolListPath = [] - } - } - } - - @Published var dialogItem: DialogItem? - - var canCreatePool: Bool { - isEnteredPoolNameValid && (enteredPoolFiat?.underlying ?? 0) > 0 - } - - var enteredPoolNameSantized: String { - enteredPoolName - .replacingOccurrences(of: "\n", with: "") - .replacingOccurrences(of: "\r", with: "") - .trimmingCharacters(in: .whitespacesAndNewlines) - } - - var isEnteredPoolNameValid: Bool { - enteredPoolNameSantized.count > 3 - } - - var enteredPoolFiat: ExchangedFiat? { - guard !enteredPoolAmount.isEmpty else { - return nil - } - - guard let amount = NumberFormatter.decimal(from: enteredPoolAmount) else { -// trace(.failure, components: "[Withdraw] Failed to parse amount string: \(enteredPoolAmount)") - return nil - } - - let currency = ratesController.entryCurrency - - guard let enteredFiat = try? Quarks(fiatDecimal: amount, currencyCode: currency, decimals: 6) else { - trace(.failure, components: "[Withdraw] Invalid amount for entry") - return nil - } - - guard let exchangedFiat = try? ratesController.exchangedFiat(for: enteredFiat) else { - trace(.failure, components: "[Withdraw] Rate not found for: \(currency)") - return nil - } - - return exchangedFiat - } - - private let container: Container - private let session: Session - private let ratesController: RatesController - private let poolController: PoolController - - // MARK: - Init - - - init(container: Container, session: Session, ratesController: RatesController, poolController: PoolController) { - self.container = container - self.session = session - self.ratesController = ratesController - self.poolController = poolController - } - - // MARK: - Pools - - - func syncPools() { - Task { - try await poolController.syncPools() - } - } - - // MARK: - Create Actions - - - func startPoolCreationFlowAction() { - - // Reset all state before - // pool creation starts - enteredPoolName = "" - enteredPoolAmount = "" - createPoolPath = [] - - isShowingCreatePoolFlow = true - } - - func submitPoolNameAction() { - navigateToEnterPoolAmount() - } - - func submitPoolAmountAction() { - guard let buyIn = enteredPoolFiat?.converted else { - return - } - - guard let limit = session.singleTransactionLimit, buyIn.quarks <= limit.quarks else { - showPoolCostTooHighError() - return - } - - navigateToPoolSummary() - } - - func createPoolAction() { - guard let buyIn = enteredPoolFiat?.converted else { - return - } - - createPoolButtonState = .loading - Task { - do { - let poolID = try await poolController.createPool( - name: enteredPoolNameSantized, - buyIn: buyIn - ) - try await Task.delay(milliseconds: 250) - - navigateToPoolDetails(poolID: poolID) - Analytics.poolCreated(id: poolID) - - createPoolButtonState = .success - try await Task.delay(milliseconds: 250) - - isShowingCreatePoolFlow = false - - try await Task.delay(milliseconds: 250) - createPoolButtonState = .normal - - } catch { - ErrorReporting.captureError(error) - createPoolButtonState = .normal - } - } - } - - // MARK: - Pool Actions - - - func selectPoolAction(poolID: PublicKey) { - navigateToPoolDetails(poolID: poolID) - } - - func selectBetAction(outcome: PoolResoltion, for pool: StoredPool) { - guard let _ = try? ratesController.exchangedFiat(for: pool.buyIn) else { - return - } - -// guard session.hasSufficientFunds(for: exchangedBuyIn) else { -// showInsufficientBalanceError() -// return -// } -// -// isShowingBetConfirmation = outcome - } - - func betAction(pool: StoredPool, outcome: PoolResoltion) async throws { - do { - try await poolController.createBet( - pool: pool, - outcome: outcome - ) - - Analytics.poolPlaceBet(id: pool.id) - - } catch { - ErrorReporting.captureError(error) - throw error - } - } - - func declarOutcomeAction(pool: StoredPool, outcome: PoolResoltion) async throws { - do { - try await poolController.declareOutcome( - pool: pool, - outcome: outcome - ) - - Analytics.poolDeclareOutcome(id: pool.id) - - } catch { - ErrorReporting.captureError(error) - throw error - } - } - - // MARK: - Presentation - - - func showPoolList() { - isShowingPoolList = true - } - - func openPoolFromDeeplink(rendezvous: KeyPair) { - updatePool( - poolID: rendezvous.publicKey, - rendezvous: rendezvous - ) - - navigateToPoolDetails(poolID: rendezvous.publicKey) - showPoolList() - - Analytics.poolOpenedFromDeeplink(id: rendezvous.publicKey) - } - - // MARK: - Updates - - - func updatePool(poolID: PublicKey, rendezvous: KeyPair?) { - Task { - try await poolController.updatePool( - poolID: poolID, - rendezvous: rendezvous - ) - } - } - - // MARK: - Create Pool Navigation - - - private func navigateToEnterPoolAmount() { - createPoolPath.append(.enterPoolAmount) - } - - private func navigateToPoolSummary() { - createPoolPath.append(.poolSummary) - } - - // MARK: - Pool List Navigation - - - private func navigateToPoolDetails(poolID: PublicKey) { - poolListPath.append(.poolDetails(poolID)) - } - - // MARK: - Errors - - - private func showPoolCostTooHighError() { - dialogItem = .init( - style: .destructive, - title: "Cost Limit Too High", - subtitle: "Your pool's cost to join is too high. Enter a smaller amount and try again.", - dismissable: true - ) { - .okay(kind: .standard) - } - } - - private func showInsufficientBalanceError() { - dialogItem = .init( - style: .destructive, - title: "Insufficient Balance", - subtitle: "You need more funds to join this Pool", - dismissable: true - ) { - .okay(kind: .destructive) - } - } -} - -// MARK: - Error - - -extension PoolViewModel { - enum Error: Swift.Error { - case insufficientFundsForBet - } -} - -// MARK: - Path - - -enum CreatePoolPath { - case enterPoolAmount - case poolSummary -} - -enum PoolListPath: Hashable { - case poolDetails(PublicKey) -} - -// MARK: - Mock - - -extension PoolViewModel { - static let mock: PoolViewModel = .init(container: .mock, session: .mock, ratesController: .mock, poolController: .mock) -} diff --git a/Flipcash/Core/Screens/Pools/PoolsScreen.swift b/Flipcash/Core/Screens/Pools/PoolsScreen.swift deleted file mode 100644 index de23cc70..00000000 --- a/Flipcash/Core/Screens/Pools/PoolsScreen.swift +++ /dev/null @@ -1,385 +0,0 @@ -// -// PoolsScreen.swift -// Code -// -// Created by Dima Bart on 2025-06-18. -// - -import SwiftUI -import FlipcashUI -import FlipcashCore - -struct PoolsScreen: View { - - @ObservedObject private var viewModel: PoolViewModel - - @StateObject private var updateablePools: Updateable<[StoredPool]> - - @State private var dialogItem: DialogItem? = nil - - private let container: Container - private let sessionContainer: SessionContainer - private let session: Session - private let database: Database - - private var pools: [StoredPool] { - updateablePools.value - } - - // MARK: - Init - - - init(container: Container, sessionContainer: SessionContainer) { - self.container = container - self.sessionContainer = sessionContainer - self.session = sessionContainer.session - self.viewModel = sessionContainer.poolViewModel - let database = sessionContainer.database - self.database = database - - _updateablePools = .init(wrappedValue: Updateable { - (try? database.getPools()) ?? [] - }) - } - - // MARK: - Lifecycle - - - private func onAppear() { - viewModel.syncPools() - } - - // MARK: - Body - - - var body: some View { - NavigationStack(path: $viewModel.poolListPath) { - Background(color: .backgroundMain) { - VStack(spacing: 0) { - if pools.isEmpty { - emptyState() - } else { - list() - } - - CodeButton( - style: .filled, - title: "Create a New Pool" -// action: viewModel.startPoolCreationFlowAction - ) { - dialogItem = .init( - style: .destructive, - title: "Pools Feature Winding Down", - subtitle: "New pools cannot be created. Existing pools should be closed out", - dismissable: true, - actions: { - .okay(kind: .standard) - } - ) - } -// .sheet(isPresented: $viewModel.isShowingCreatePoolFlow) { -// EnterPoolNameScreen( -// isPresented: $viewModel.isShowingCreatePoolFlow, -// viewModel: viewModel -// ) -// } - .padding(.horizontal, 20) - .padding(.bottom, 20) - } - .navigationTitle("Pools") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - ToolbarCloseButton(binding: $viewModel.isShowingPoolList) - } - } - } - .dialog(item: $dialogItem) - .ignoresSafeArea(.keyboard) - .onAppear(perform: onAppear) - .navigationDestination(for: PoolListPath.self) { path in - switch path { - case .poolDetails(let poolID): - PoolDetailsScreen( - userID: session.userID, - poolID: poolID, - database: database, - viewModel: viewModel - ) - } - } - } - } - - @ViewBuilder private func emptyState() -> some View { - VStack(spacing: 20) { - Spacer() - - VStack(spacing: 10) { - Image.asset(.graphicPoolPlaceholder) - .overlay { - TextCarousel( - interval: 2, - items: [ - "Will Joe and Sally\nhave a baby girl?", - "Will David get a\ngirl's number tonight?", - "Will Jack bring a\ndate to the wedding?", - "Will Caleb dunk\nthis basketball?", - "Will Jill text her\nex before dawn?", - ] - ) - .font(.appTextLarge) - .foregroundStyle(Color.white.opacity(0.5)) - .multilineTextAlignment(.center) - .offset(y: -80) - } - - Text("Create a pool, collect money from your friends, and then decide who was right!") - .font(.appTextMedium) - .foregroundStyle(Color.textSecondary) - .multilineTextAlignment(.center) - } - - Spacer() - } - .padding(20) - .foregroundStyle(Color.textMain) - } - - @ViewBuilder private func list() -> some View { - let openPools = pools.filter { $0.resolution == nil } - let completedPools = pools.filter { $0.resolution != nil } - GeometryReader { g in - List { - if !openPools.isEmpty { - section( - name: "Open", - pools: openPools - ) - } - - if !completedPools.isEmpty { - section( - name: "Completed", - pools: completedPools - ) - } - } - .listStyle(.grouped) - .scrollContentBackground(.hidden) - } - } - - @ViewBuilder private func section(name: String, pools: [StoredPool]) -> some View { - Section { - ForEach(pools) { pool in - row(pool: pool) - } - } header: { - HeadingBadge(title: name) - .textCase(nil) - .padding(.horizontal, 20) - .padding(.bottom, 10) - .padding(.top, 20) - } - //.listSectionSeparator(.hidden) - .listRowInsets(EdgeInsets()) - .listRowSeparatorTint(.rowSeparator) - } - - @ViewBuilder private func row(pool: StoredPool) -> some View { - Button { - viewModel.selectPoolAction(poolID: pool.id) - } label: { - HStack(spacing: 10) { - VStack(alignment: .leading, spacing: 5) { - if pool.isHost { - HStack(spacing: 5) { - Image.system(.person) - .font(.appTextSmall) - Text("Host") - .font(.appTextMedium) - } - .foregroundStyle(Color.textSecondary) - } - - Text(pool.name) - .font(.appTextLarge) - .foregroundStyle(Color.textMain) - .multilineTextAlignment(.leading) - - HStack(spacing: 8) { - if let resolution = pool.resolution { - ResolutionBadge(resolution: resolution) - - switch pool.userOutcome { - case .none: - EmptyView() - case .won(let amount): - if amount.quarks > 0 { - ResultBadge( - style: .won, - text: "Won", - amount: amount - ) - } - - case .lost(let amount): - if amount.quarks > 0 { - ResultBadge( - style: .lost, - text: "Lost", - amount: amount - ) - } - - case .refunded: - ResultBadge( - style: .tied, - text: "Tie", - amount: nil - ) - } - - } else { - Text("\(pool.amountInPool.formatted()) in pool so far") - .font(.appTextMedium) - .foregroundStyle(Color.textSecondary) - } - - if BetaFlags.shared.hasEnabled(.showMissingRendezvous), pool.rendezvous == nil { - ResultBadge( - style: .lost, - text: "Missing Rendezvous", - amount: nil - ) - } - } - } - .padding(.top, 2) - .frame(maxWidth: .infinity, alignment: .leading) - - Image.system(.chevronRight) - .foregroundStyle(Color.textSecondary) - } - } - .listRowBackground(Color.clear) - .padding(.horizontal, 20) - .padding(.vertical, 15) - } -} - -// MARK: - HeadingBadge - - -struct HeadingBadge: View { - - let title: String - - init(title: String) { - self.title = title - } - - var body: some View { - Text(title) - .font(.appTextMedium) - .foregroundStyle(Color.textMain.opacity(0.5)) - .padding(.horizontal, 12) - .frame(height: 32) - .background { - RoundedRectangle(cornerRadius: 99) - .fill(Color.white.opacity(0.12)) - } - } -} - -// MARK: - ResolutionBadge - - -struct ResolutionBadge: View { - - let resolution: PoolResoltion - - init(resolution: PoolResoltion) { - self.resolution = resolution - } - - var body: some View { - Text("Result: \(resolution.name)") - .font(.appTextSmall) - .foregroundStyle(Color.textMain.opacity(0.5)) - .padding(.horizontal, 6) - .frame(height: 26) - .background { - RoundedRectangle(cornerRadius: 5) - .fill(Color.white.opacity(0.11)) - } - } -} - -// MARK: - ResultBadge - - -extension Color { - static let badgeWonBackground = Color(r: 44, g: 77, b: 54) - static let badgeLostBackground = Color(r: 64, g: 44, b: 35) - static let badgeNormalBackground = Color(r: 33, g: 50, b: 40) -} - -struct ResultBadge: View { - - let style: Style - let text: String - let amount: Quarks? - - init(style: Style, text: String, amount: Quarks?) { - self.style = style - self.text = text - self.amount = amount - } - - var body: some View { - HStack(spacing: 5) { - Text(text) - if let amount { - Text(amount.formatted()) - } - } - .font(.appTextSmall) - .padding(.horizontal, 6) - .frame(height: 26) - .foregroundStyle(style.textColor) - .background { - RoundedRectangle(cornerRadius: 5) - .fill(style.backgroundColor) - } - } -} - -extension ResultBadge { - enum Style { - case won - case lost - case tied - - var textColor: Color { - switch self { - case .won: return Color(r: 115, g: 234, b: 164) - case .lost: return Color(r: 214, g: 94, b: 89) - case .tied: return .textMain.opacity(0.5) - } - } - - var backgroundColor: Color { - switch self { - case .won: return .badgeWonBackground - case .lost: return .badgeLostBackground - case .tied: return .badgeNormalBackground - } - } - } -} - -extension PoolResoltion { - var name: String { - switch self { - case .yes: return "Yes" - case .no: return "No" - case .refund: return "Tie" - } - } -} diff --git a/Flipcash/Core/Session/SessionAuthenticator.swift b/Flipcash/Core/Session/SessionAuthenticator.swift index dd4a7872..3a7ae468 100644 --- a/Flipcash/Core/Session/SessionAuthenticator.swift +++ b/Flipcash/Core/Session/SessionAuthenticator.swift @@ -191,23 +191,6 @@ final class SessionAuthenticator: ObservableObject { userID: initializedAccount.userID ) - let poolController = PoolController( - container: container, - session: session, - ratesController: ratesController, - keyAccount: initializedAccount.keyAccount, - owner: owner, - userID: initializedAccount.userID, - database: database - ) - - let poolViewModel = PoolViewModel( - container: container, - session: session, - ratesController: ratesController, - poolController: poolController - ) - let onrampViewModel = OnrampViewModel( container: container, session: session, @@ -223,8 +206,6 @@ final class SessionAuthenticator: ObservableObject { ratesController: ratesController, historyController: historyController, pushController: pushController, - poolController: poolController, - poolViewModel: poolViewModel, onrampViewModel: onrampViewModel ) } @@ -381,15 +362,13 @@ final class SessionAuthenticator: ObservableObject { // MARK: - SessionContainer - struct SessionContainer { - + let session: Session let database: Database let walletConnection: WalletConnection let ratesController: RatesController let historyController: HistoryController let pushController: PushController - let poolController: PoolController - let poolViewModel: PoolViewModel let onrampViewModel: OnrampViewModel fileprivate func injectingEnvironment(into view: SomeView) -> some View where SomeView: View { @@ -402,7 +381,7 @@ struct SessionContainer { } @MainActor - static let mock: SessionContainer = .init(session: .mock, database: .mock, walletConnection: .mock, ratesController: .mock, historyController: .mock, pushController: .mock, poolController: .mock, poolViewModel: .mock, onrampViewModel: .mock) + static let mock: SessionContainer = .init(session: .mock, database: .mock, walletConnection: .mock, ratesController: .mock, historyController: .mock, pushController: .mock, onrampViewModel: .mock) } extension View { diff --git a/Flipcash/Supporting Files/apple-app-site-association b/Flipcash/Supporting Files/apple-app-site-association index 5244e4a7..10af7c5b 100644 --- a/Flipcash/Supporting Files/apple-app-site-association +++ b/Flipcash/Supporting Files/apple-app-site-association @@ -20,14 +20,6 @@ "/": "/c/*", "comment": "Support for sends" }, - { - "/": "/pool/*", - "comment": "Support for pools" - }, - { - "/": "/p/*", - "comment": "Support for pools" - }, { "/": "/verify/*", "comment": "Support for email verification" diff --git a/Flipcash/UI/ConfettiBox.swift b/Flipcash/UI/ConfettiBox.swift deleted file mode 100644 index 49023047..00000000 --- a/Flipcash/UI/ConfettiBox.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// ConfettiBox.swift -// Code -// -// Created by Dima Bart on 2025-07-15. -// - -import SwiftUI -import ConfettiSwiftUI - -struct ConfettiBox: View { - - @Binding var trigger: Int - - init(trigger: Binding) { - self._trigger = trigger - } - - var body: some View { - VStack { - - } - .confettiCannon( - trigger: $trigger, - num: 100, - confettis: [ -// .shape(.circle), -// .shape(.triangle), - .shape(.square), - .shape(.slimRectangle), -// .shape(.roundedCross), - ], -// colors: <#T##[Color]#>, - confettiSize: 10, -// rainHeight: <#T##CGFloat#>, - fadesOut: true, - opacity: 1.0, - openingAngle: .degrees(45), - closingAngle: .degrees(135), -// radius: <#T##CGFloat#>, - repetitions: 3, - repetitionInterval: 0.7, - hapticFeedback: true - ) - } -} diff --git a/Flipcash/UI/Modals/ModalSwipeToBet.swift b/Flipcash/UI/Modals/ModalSwipeToBet.swift deleted file mode 100644 index 483822d7..00000000 --- a/Flipcash/UI/Modals/ModalSwipeToBet.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// ModalSwipeToBet.swift -// Code -// -// Created by Dima Bart on 2025-06-26. -// - -import SwiftUI -import FlipcashUI -import FlipcashCore - -public struct ModalSwipeToBet: View { - - public let fiat: Quarks - public let subtext: String - public let swipeText: String - public let cancelTitle: String - public let paymentAction: ThrowingAction - public let dismissAction: VoidAction - public let cancelAction: VoidAction - - // MARK: - Init - - - public init(fiat: Quarks, subtext: String, swipeText: String, cancelTitle: String, paymentAction: @escaping ThrowingAction, dismissAction: @escaping VoidAction, cancelAction: @escaping VoidAction) { - self.fiat = fiat - self.subtext = subtext - self.swipeText = swipeText - self.cancelTitle = cancelTitle - self.paymentAction = paymentAction - self.dismissAction = dismissAction - self.cancelAction = cancelAction - } - - // MARK: - Body - - - public var body: some View { - VStack(spacing: 10) { - - VStack(spacing: 10) { - AmountText( - flagStyle: fiat.currencyCode.flagStyle, - content: fiat.formatted() - ) - .font(.appDisplayMedium) - .foregroundStyle(Color.textMain) - .padding(.top, 20) - - Text(subtext) - .font(.appTextMedium) - .foregroundStyle(Color.textSecondary) - } - - VStack { - SwipeControl( - style: .green, - text: swipeText, - action: { - try await paymentAction() - try await Task.delay(milliseconds: 500) - }, - completion: { - dismissAction() - try await Task.delay(milliseconds: 1000) // Checkmark delay - } - ) - - CodeButton( - style: .subtle, - title: cancelTitle, - action: { - cancelAction() - } - ) - .padding(.bottom, -20) - } - .padding(.top, 25) - } - .padding(20) - .foregroundColor(.textMain) - .font(.appTextMedium) - .background(Color.backgroundMain) - } -} diff --git a/Flipcash/UI/Modals/ModalSwipeToDeclareWinner.swift b/Flipcash/UI/Modals/ModalSwipeToDeclareWinner.swift deleted file mode 100644 index a660ca5c..00000000 --- a/Flipcash/UI/Modals/ModalSwipeToDeclareWinner.swift +++ /dev/null @@ -1,131 +0,0 @@ -// -// ModalSwipeToDeclareWinner.swift -// Code -// -// Created by Dima Bart on 2025-06-26. -// - -import SwiftUI -import FlipcashUI -import FlipcashCore - -public struct ModalSwipeToDeclareWinner: View { - - public let outcome: PoolResoltion - public let amount: Quarks - public let swipeText: String - public let cancelTitle: String - public let paymentAction: ThrowingAction - public let dismissAction: VoidAction - public let cancelAction: VoidAction - - // MARK: - Init - - - public init(outcome: PoolResoltion, amount: Quarks, swipeText: String, cancelTitle: String, paymentAction: @escaping ThrowingAction, dismissAction: @escaping VoidAction, cancelAction: @escaping VoidAction) { - self.outcome = outcome - self.amount = amount - self.swipeText = swipeText - self.cancelTitle = cancelTitle - self.paymentAction = paymentAction - self.dismissAction = dismissAction - self.cancelAction = cancelAction - } - - // MARK: - Body - - - public var body: some View { - VStack(spacing: 10) { - - VStack(spacing: 10) { - Text(outcome.text) - .font(.appDisplayMedium) - .foregroundStyle(Color.textMain) - Text("\(outcome.subtext) \(amount.formatted())") - .font(.appTextSmall) - .foregroundStyle(Color.textMain.opacity(0.5)) - } - .frame(height: 150) - .frame(maxWidth: .infinity) - .background { - RoundedRectangle(cornerRadius: Metrics.boxRadius) - .fill(outcome.fillColor) - .strokeBorder(outcome.strokeColor, lineWidth: 1) - } - .padding(.top, 10) - - VStack { - SwipeControl( - style: .green, - text: swipeText, - action: { - try await paymentAction() - try await Task.delay(milliseconds: 500) - }, - completion: { - dismissAction() - try await Task.delay(milliseconds: 1000) // Checkmark delay - } - ) - - CodeButton( - style: .subtle, - title: cancelTitle, - action: { - cancelAction() - } - ) - .padding(.bottom, -20) - } - .padding(.top, 15) - } - .padding(20) - .foregroundColor(.textMain) - .font(.appTextMedium) - .background(Color.backgroundMain) - } -} - -// MARK: - PoolResoltion - - -extension PoolResoltion { - - var strokeColor: Color { - switch self { - case .yes, .no: - Color(r: 77, g: 153, b: 97) - case .refund: - .lightStroke - } - } - - var fillColor: Color { - switch self { - case .yes, .no: - .winnerGreen - case .refund: - .extraLightFill - } - } - - var text: String { - switch self { - case .yes: - "Yes" - case .no: - "No" - case .refund: - "Tie" - } - } - - var subtext: String { - switch self { - case .yes: - "Each winner receives" - case .no: - "Each winner receives" - case .refund: - "Everyone receives" - } - } -} diff --git a/Flipcash/UI/TextCarousel.swift b/Flipcash/UI/TextCarousel.swift index 4463d145..95541c87 100644 --- a/Flipcash/UI/TextCarousel.swift +++ b/Flipcash/UI/TextCarousel.swift @@ -79,9 +79,9 @@ extension TextCarousel { TextCarousel( interval: 5.0, items: [ - "0: Will Jimmy and Sally have a girl?", - "1: Will the Pacers win the NBA Finals?", - "2: Will Flipcash pools launch in June?", + "First item in the carousel", + "Second item in the carousel", + "Third item in the carousel", ] ) } diff --git a/Flipcash/Utilities/Events.swift b/Flipcash/Utilities/Events.swift index cb15a979..5944b9bc 100644 --- a/Flipcash/Utilities/Events.swift +++ b/Flipcash/Utilities/Events.swift @@ -38,34 +38,6 @@ extension Analytics { } } -// MARK: - Pools - - -extension Analytics { - static func poolOpenedFromDeeplink(id: PublicKey) { - track(event: .poolOpened, properties: [ - .id: id.base58 - ]) - } - - static func poolCreated(id: PublicKey) { - track(event: .poolCreated, properties: [ - .id: id.base58 - ]) - } - - static func poolPlaceBet(id: PublicKey) { - track(event: .poolPlaceBet, properties: [ - .id: id.base58 - ]) - } - - static func poolDeclareOutcome(id: PublicKey) { - track(event: .poolDeclareOutcome, properties: [ - .id: id.base58 - ]) - } -} - // MARK: - Cash Transfer - extension Analytics { @@ -265,15 +237,10 @@ extension Analytics { case buttonAllowCamera = "Button: Allow Camera" case buttonAllowPush = "Button: Allow Push" case buttonSkipPush = "Button: Skip Push" - + case autoLoginComplete = "Auto-login complete" case completeOnboarding = "Complete Onboarding" - - case poolOpened = "Pool: Opened From Deeplink" - case poolCreated = "Pool: Created" - case poolDeclareOutcome = "Pool: Declare Outcome" - case poolPlaceBet = "Pool: Place Bet" - + case onrampOpenedFromSettings = "Onramp: Opened From Settings" case onrampOpenedFromBalance = "Onramp: Opened From Balance" case onrampOpenedFromGive = "Onramp: Opened From Give" diff --git a/Flipcash/Utilities/URL+Links.swift b/Flipcash/Utilities/URL+Links.swift index 0e5d4b91..b38ba629 100644 --- a/Flipcash/Utilities/URL+Links.swift +++ b/Flipcash/Utilities/URL+Links.swift @@ -16,16 +16,7 @@ extension URL { static func cashLink(with mnemonic: MnemonicPhrase) -> URL { URL(string: "https://send.flipcash.com/c/#/e=\(mnemonic.base58EncodedEntropy)")! } - - static func poolLink(rendezvous: KeyPair, info: PoolInfo?) -> URL { - var infoString: String = "" - if let info, let encoded = try? Data.base64EncodedPoolInfo(info) { - infoString = "\(encoded)/" - } - - return URL(string: "https://fun.flipcash.com/p/\(infoString)#/e=\(rendezvous.seed!.base58)")! - } - + static var privacyPolicy: URL { URL(string: "https://www.flipcash.com/privacy")! }