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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion CrossPlatformVectors/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ let sharedCore: Package.Dependency = {
if let sharedCoreLocalRoot {
return .package(path: "\(sharedCoreLocalRoot)/kmp/shared-core/spm")
}
return .package(url: "https://github.com/code-payments/flipcash-shared-core-spm", .upToNextMinor(from: "0.17.0"))
return .package(url: "https://github.com/code-payments/flipcash-shared-core-spm", .upToNextMinor(from: "0.18.0"))
}()

// Cross-platform test-vector GATE (Track-B/C safety net). Asserts the ed25519 this app signs
Expand Down
3 changes: 2 additions & 1 deletion Flipcash/Core/Screens/Conversation/ConversationScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1434,7 +1434,8 @@ struct ConversationScreen: View {
conversationID: conversationID,
messageID: message.id,
messageSender: sender,
chat: chat
chat: chat,
conversationType: conversationController.conversation(withID: conversationID)?.type
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ struct ReactionBoostTarget: Hashable, Identifiable {
let messageID: MessageID
let messageSender: UserID
let chat: ReactionBoostSplit.Chat
/// The conversation's type, for analytics only. Nil when it isn't known locally.
var conversationType: ConversationType?

var id: Self { self }
}
Expand Down Expand Up @@ -241,6 +243,7 @@ final class ReactionBoostViewModel {
try await multiSender.sendMultiple(payments, verifiedState: pin, chat: chat)
}
} catch {
Analytics.sentBoost(chatType: target.conversationType, exchangedFiat: total, error: error)
if !(error is CancellationError) {
showSendError()
}
Expand All @@ -253,6 +256,7 @@ final class ReactionBoostViewModel {
messageID: target.messageID,
in: target.conversationID
)
Analytics.sentBoost(chatType: target.conversationType, exchangedFiat: total, error: nil)
return .success
}

Expand Down
11 changes: 10 additions & 1 deletion Flipcash/Core/Screens/Invite Links/GroupInviteLinks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ final class GroupInviteLinks {
// Only a transport failure is worth retrying with the same token; any answer from the
// server is final for this secret.
if error != .transportFailure { pendingSecret = nil }
Analytics.groupInviteLinkCreated(error: error)
throw error
}
pendingSecret = nil
Expand All @@ -252,12 +253,14 @@ final class GroupInviteLinks {
logger.warning("CreateInvite returned an unusable invite", metadata: [
"status": "\(invite.status)",
])
Analytics.groupInviteLinkCreated(error: CreatedUnusable())
throw CreatedUnusable()
}

let entry = Entry(invite: invite, url: url)
entries.removeAll { $0.id == entry.id }
entries.insert(entry, at: 0)
Analytics.groupInviteLinkCreated(error: nil)
return entry
}

Expand All @@ -269,7 +272,13 @@ final class GroupInviteLinks {
revoking.insert(entry.id)
defer { revoking.remove(entry.id) }

try await service.revoke(conversationID, entry.id)
do {
try await service.revoke(conversationID, entry.id)
} catch {
Analytics.groupInviteLinkRevoked(error: error)
throw error
}
Analytics.groupInviteLinkRevoked(error: nil)
guard let index = entries.firstIndex(where: { $0.id == entry.id }) else { return }
let old = entries[index].invite
entries[index] = Entry(
Expand Down
14 changes: 12 additions & 2 deletions Flipcash/Core/Screens/Join Requests/HostedLobbies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,26 @@ final class HostedLobbies {
let envelope = try await wrapKey(member.publicKey, conversationID)
try await admitMember(conversationID, userID, envelope)
} catch ErrorAdmitLobbyMember.notInLobby {
// Withdrawn, or settled on another device.
// Withdrawn, or settled on another device. Counts as admitted.
} catch {
Analytics.groupJoinRequestApproved(error: error)
throw error
}
remove(userID, from: conversationID)
Analytics.groupJoinRequestApproved(error: nil)
}

/// Turns `member` away. The requester isn't told.
func deny(_ member: LobbyMember, in conversationID: ConversationID) async throws {
guard let userID = member.userID else { return }
try await denyMember(conversationID, userID)
do {
try await denyMember(conversationID, userID)
} catch {
Analytics.groupJoinRequestDenied(error: error)
throw error
}
remove(userID, from: conversationID)
Analytics.groupJoinRequestDenied(error: nil)
}

private func remove(_ userID: UserID, from conversationID: ConversationID) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ class CurrencyLaunchProcessingViewModel {
var isFinished: Bool { displayState != .processing }
var isSuccess: Bool { displayState == .success }

var analyticsEvent: Analytics.CurrencyLaunchEvent {
paymentMint == .usdf ? .launchWithReserves : .launchWithCurrency
var launchMethod: LaunchMethod {
paymentMint == .usdf ? .reserves : .currency
}

// MARK: - Actions -
Expand Down Expand Up @@ -250,7 +250,7 @@ class CurrencyLaunchProcessingViewModel {
}

private func trackLaunch(successful: Bool, error: Error? = nil) {
Analytics.currencyLaunch(event: analyticsEvent, launchedMint: launchedMint, paymentMint: paymentMint, exchangedFiat: launchAmount, successful: successful, error: error)
Analytics.currencyLaunch(method: launchMethod, launchedMint: launchedMint, paymentMint: paymentMint, exchangedFiat: launchAmount, successful: successful, error: error)
}

/// Polls `session.balance(for: launchedMint)` until the launched currency
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ struct TopCommunityChatsDestination: View {
chats: viewModel.communityChats,
subtitle: viewModel.communitySubtitle(for:),
onSelect: { chat in
Analytics.tokenInfoCommunityChatTapped(mint: mint)
conversationController.hold(chat)
router.push(.chatProfile(chat.id, origin: .featuredGroup))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,27 +311,31 @@ struct CurrencyInfoContentV2: View {
ForEach(CurrencyInfoActions.tiles(isOwned: isOwned, isUSDF: isUSDF), id: \.self) { tile in
switch tile {
case .send:
actionTile("Send", icon: .asset("IconTileSend"), action: onSend)
actionTile("Send", tile: tile, icon: .asset("IconTileSend"), action: onSend)
case .buy:
actionTile("Buy", icon: .asset("IconTileBuy"), action: onBuy)
actionTile("Buy", tile: tile, icon: .asset("IconTileBuy"), action: onBuy)
case .buyIn:
actionTile("Buy In", icon: .asset("IconTileBuy"), action: onBuy)
actionTile("Buy In", tile: tile, icon: .asset("IconTileBuy"), action: onBuy)
case .transactions:
actionTile("Transactions", icon: .system("clock.arrow.circlepath"), action: onShowTransactionHistory)
actionTile("Transactions", tile: tile, icon: .system("clock.arrow.circlepath"), action: onShowTransactionHistory)
case .more:
actionTile("More", icon: .system("ellipsis"), action: onMore)
actionTile("More", tile: tile, icon: .system("ellipsis"), action: onMore)
}
}
}
}

private func actionTile(
_ title: String,
tile: CurrencyInfoActions.Tile,
icon: TileIcon,
isEnabled: Bool = true,
action: @escaping () -> Void
) -> some View {
Button(action: action) {
Button {
Analytics.tokenInfoTileTapped(tile: tile, mint: metadata.mint)
action()
} label: {
VStack(spacing: 8) {
tileIcon(icon)
.frame(height: 24)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ private struct CurrencyInfoScreenContent: View {
onShowTransactionHistory: { router.push(.transactionHistory(metadata.mint)) },
onMore: { isShowingMore = true },
onSelectCommunityChat: { chat in
Analytics.tokenInfoCommunityChatTapped(mint: metadata.mint)
conversationController.hold(chat)
router.push(.chatProfile(chat.id, origin: .featuredGroup))
},
Expand Down Expand Up @@ -390,6 +391,7 @@ private struct CurrencyInfoScreenContent: View {
private func runPendingMoreRow() {
guard let row = pendingMoreRow else { return }
pendingMoreRow = nil
Analytics.tokenInfoMoreOptionPicked(row: row, mint: mint)
switch row {
case .convert: router.push(.convertCurrency(mint))
case .withdraw: router.push(.withdrawCurrency(mint))
Expand Down
2 changes: 1 addition & 1 deletion Flipcash/Core/Screens/Main/ScanViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ class ScanViewModel {
switch outcome {
case .code(let code, let match):
Analytics.galleryScanFoundCode(
tier: match.tier.rawValue,
tier: match.tier,
zoom: Double(match.zoom),
elapsed: elapsed
)
Expand Down
11 changes: 10 additions & 1 deletion Flipcash/Core/Screens/Main/Search/ChatSearchScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,16 @@ struct ChatSearchScreen: View {
.animation(.easeInOut(duration: 0.2), value: model?.isIdle ?? true)
}
.toolbar(.hidden, for: .navigationBar)
.onChange(of: isActive) { _, _ in startFresh() }
.onChange(of: isActive) { _, active in
if active { Analytics.searchOpened() }
startFresh()
}
.onChange(of: scenePhase) { _, phase in
if phase == .background { startFresh() }
}
.task {
guard model == nil else { return }
if isActive { Analytics.searchOpened() }
let model = makeModel()
self.model = model
await model.load()
Expand Down Expand Up @@ -179,16 +183,19 @@ struct ChatSearchScreen: View {
}

private func open(_ conversation: Conversation) {
Analytics.searchResultTapped(.chat)
sessionContainer.chatSearchRecents.record(conversation.id)
push(.tipConversation(conversation.id))
}

private func open(_ hit: ChatSearchMessageHit) {
Analytics.searchResultTapped(.message)
sessionContainer.chatSearchRecents.record(hit.conversationID)
push(.chatMessage(hit.conversationID, MessageLanding(messageID: hit.messageID, returnTo: nil)))
}

private func open(_ link: ChatSearchLink) {
Analytics.searchResultTapped(.link)
ExternalLinkOpener(session: session, trustedWebsites: container.trustedWebsites).open(link.url)
}

Expand All @@ -197,6 +204,7 @@ struct ChatSearchScreen: View {
///
/// The keyboard goes down first, so it doesn't slide away over the currency.
private func openCurrency(_ mint: MintMetadata) {
Analytics.searchResultTapped(.currency)
guard isFieldFocused else {
showCurrency(mint)
return
Expand All @@ -214,6 +222,7 @@ struct ChatSearchScreen: View {
if chrome == .pushed {
router.pop(on: .tips)
}
Analytics.tokenInfoOpened(from: .openedFromSearch, mint: mint.address)
router.navigate(to: .currencyInfo(mint.address))
}
}
Expand Down
4 changes: 2 additions & 2 deletions Flipcash/Core/Screens/Main/Tips/NewChatScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ struct NewChatScreen: View {
title: "Create a New Group",
accessibilityIdentifier: "new-chat-private-group"
) {
Analytics.groupNewOpened()
Analytics.groupNewOpened(type: .private_)
router.push(.newPrivateGroup)
}

Expand Down Expand Up @@ -68,7 +68,7 @@ struct NewChatScreen: View {
return
}

Analytics.groupNewOpened()
Analytics.groupNewOpened(type: .community)
router.push(.newPublicGroup)
}
}
16 changes: 14 additions & 2 deletions Flipcash/Core/Screens/Moderation/GroupModeration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,13 @@ struct GroupModeration {
var didRemove: @MainActor (UserID) -> Void = { _ in }

func silence(_ userID: UserID, firstName: String, duration: SilenceDuration, now: Date = .now) async throws {
try await moderation.silenceUser(owner: owner, conversationID: conversationID, userID: userID, silence: duration.silence(from: now))
do {
try await moderation.silenceUser(owner: owner, conversationID: conversationID, userID: userID, silence: duration.silence(from: now))
} catch {
Analytics.groupMemberSilenced(duration: duration, error: error)
throw error
}
Analytics.groupMemberSilenced(duration: duration, error: nil)
announce(GroupModerationCopy.silencedToast(firstName: firstName, duration: duration))
}

Expand All @@ -138,7 +144,13 @@ struct GroupModeration {
/// Puts `userID` out of the group and bars them from rejoining. The roster drops them through
/// the `MemberLeft` update the other members get, so nothing local needs to move.
func remove(_ userID: UserID) async throws {
try await moderation.banUser(owner: owner, conversationID: conversationID, userID: userID)
do {
try await moderation.banUser(owner: owner, conversationID: conversationID, userID: userID)
} catch {
Analytics.groupMemberRemoved(error: error)
throw error
}
Analytics.groupMemberRemoved(error: nil)
didRemove(userID)
}

Expand Down
4 changes: 1 addition & 3 deletions Flipcash/Core/Screens/Onboarding/OnboardingViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,7 @@ class OnboardingViewModel {
// MARK: - Registration -

private func registerAccount(mnemonic: MnemonicPhrase) async throws {
let owner = mnemonic.solanaKeyPair()

Analytics.createAccount(owner: owner.publicKey)
Analytics.createAccount()

let account = try await sessionAuthenticator.initialize(
using: mnemonic,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,12 @@ final class PaymentRequestCreateViewModel {
do {
request = try await create(amount: amount, people: people, key: idempotencyKey, now: now)
created = request
Analytics.paymentRequestCreated(payerCount: people.askedCount, amount: amount, error: nil)
} catch let error as ErrorCreatePaymentRequest {
Analytics.paymentRequestCreated(payerCount: people.askedCount, amount: amount, error: error)
return .failed(alert(for: error))
} catch {
Analytics.paymentRequestCreated(payerCount: people.askedCount, amount: amount, error: error)
ErrorReporting.captureError(error, reason: "Failed to create payment request")
return .failed(Self.genericError)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ struct PaymentRequestDetailsScreen: View {
subtitle: "This will stop payments. Anyone who hasn't paid will be marked Didn't pay. Payments already made won't be affected."
) {
DialogAction.destructive("Close Request") {
Task { await update { try await $0.close(requestID) } }
Task { await close() }
}
DialogAction.cancel()
}
Expand Down Expand Up @@ -303,6 +303,18 @@ struct PaymentRequestDetailsScreen: View {
}

/// Runs a creator action; the store takes the server's response, and a failure refetches.
private func close() async {
await update { requests in
do {
try await requests.close(requestID)
Analytics.paymentRequestClosed(error: nil)
} catch {
Analytics.paymentRequestClosed(error: error)
throw error
}
}
}

private func update(_ action: (PaymentRequests) async throws -> Void) async {
isUpdating = true
defer { isUpdating = false }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,11 @@ final class PaymentRequestPayment {
intentID: intentKey
)
self.intentID = nil
Analytics.transfer(event: .sentCash, exchangedFiat: amount, grabTime: nil, successful: true, error: nil)
Analytics.sentCash(exchangedFiat: amount, context: .paymentRequest, error: nil)
try? await requests.refresh(requestID)
return .success
} catch {
Analytics.transfer(event: .sentCash, exchangedFiat: amount, grabTime: nil, successful: false, error: error)
Analytics.sentCash(exchangedFiat: amount, context: .paymentRequest, error: error)
needsRefetch = true
guard !(error is CancellationError) else { return .failed }
// A refusal because the request ended reads as "Request Closed", not a send failure.
Expand Down
Loading
Loading