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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.flipcash.app.core.navigation

import android.net.Uri
import android.os.Parcelable
import com.flipcash.app.core.chat.ChatIdentifier
import com.flipcash.services.models.chat.ChatId
import com.getcode.solana.keys.Mint
import kotlinx.parcelize.Parcelize
Expand All @@ -16,7 +17,7 @@ sealed interface DeeplinkType: Parcelable {

@Serializable data class TokenInfo(val mint: Mint): DeeplinkType, Navigatable

@Serializable data class Chat(val chatId: ChatId): DeeplinkType, Navigatable
@Serializable data class Chat(val identifier: ChatIdentifier): DeeplinkType, Navigatable

@Serializable
data class EmailVerification(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ object Linkify {
fun tweet(message: String): String = "https://www.twitter.com/intent/tweet?text=${message.urlEncode()}"
fun tokenInfo(token: Token): String = tokenInfo(token.address)
fun tokenInfo(mint: Mint): String = "https://app.flipcash.com/token/${mint.base58()}"
fun chat(chatId: ChatId): String = "https://app.flipcash.com/chat/${chatId.bytes.encodeBase64(urlSafe = true)}"
fun chatById(chatId: ChatId): String = "https://app.flipcash.com/chat/${chatId.bytes.encodeBase64(urlSafe = true)}"
fun chatByPhone(phoneNumber: String): String = "https://app.flipcash.com/chat/${phoneNumber.urlEncode()}"
}
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,10 @@ internal class ChatViewModel @Inject constructor(
// 2. Resolve contact
when (identifier) {
is ChatIdentifier.ByContact -> {
dispatchEvent(Event.OnContactFound(identifier.contact))
val resolved = contactCoordinator.lookupContact(identifier.contact.e164).getOrNull()
?: chatId?.let { contactCoordinator.lookupContactByDmChatId(it.toString()) }
?: identifier.contact
dispatchEvent(Event.OnContactFound(resolved))
}
is ChatIdentifier.ByChatId -> {
val contact = contactCoordinator.lookupContactByDmChatId(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,14 @@ class NotificationService : FirebaseMessagingService(),
}
}

if (payload?.navigation is NavigationTrigger.Chat) {
launch {
chatCoordinator.refreshFeed()
chatCoordinator.loadMessages(chatId = (payload.navigation as NavigationTrigger.Chat).chatId)
when (val trigger = payload?.navigation) {
is NavigationTrigger.Chat.ById -> {
launch {
chatCoordinator.refreshFeed()
chatCoordinator.loadMessages(chatId = trigger.chatId)
}
}
else -> Unit
}

authenticateIfNeeded {
Expand Down Expand Up @@ -158,7 +161,13 @@ class NotificationService : FirebaseMessagingService(),
val channel = NotificationChannels.channelFor(this, category)
notificationManager.createNotificationChannel(channel)

val chatId = (payload?.navigation as? NavigationTrigger.Chat)?.chatId
val chatId = when (val trigger = payload?.navigation) {
is NavigationTrigger.Chat.ByContact -> null
is NavigationTrigger.Chat.ById -> trigger.chatId
is NavigationTrigger.CurrencyInfo -> null
null -> null
}

if (chatId != null && chatCoordinator.isActiveChat(chatId)) return

val groupKey = payload?.groupKey?.takeIf { it.isNotEmpty() }
Expand Down Expand Up @@ -339,8 +348,12 @@ class NotificationService : FirebaseMessagingService(),
data = Linkify.tokenInfo(navigation.mint).toUri()
}

is NavigationTrigger.Chat -> Intent(Intent.ACTION_VIEW).apply {
data = Linkify.chat(navigation.chatId).toUri()
is NavigationTrigger.Chat.ById -> Intent(Intent.ACTION_VIEW).apply {
data = Linkify.chatById(navigation.chatId).toUri()
}

is NavigationTrigger.Chat.ByContact -> Intent(Intent.ACTION_VIEW).apply {
data = Linkify.chatByPhone(navigation.phoneNumber).toUri()
}

else -> packageManager.getLaunchIntentForPackage(packageName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.flipcash.app.router.internal
import androidx.core.net.toUri
import com.flipcash.app.core.AppRoute
import com.flipcash.app.core.chat.ChatIdentifier
import com.flipcash.app.core.contacts.DeviceContact
import com.flipcash.app.core.navigation.DeeplinkAction
import com.flipcash.app.core.navigation.DeeplinkType
import com.flipcash.services.models.chat.ChatId
Expand Down Expand Up @@ -60,7 +61,7 @@ internal class AppRouter(

is DeeplinkType.EmailVerification -> resolveEmailVerification(type)
is DeeplinkType.Chat -> DeeplinkAction.Navigate(
listOf(AppRoute.Sheets.Send(), AppRoute.Messaging.Chat(ChatIdentifier.ByChatId(type.chatId)))
listOf(AppRoute.Sheets.Send(), AppRoute.Messaging.Chat(type.identifier))
)
}
}
Expand Down Expand Up @@ -151,11 +152,22 @@ private fun DeepLink.handleTokenLink(): DeeplinkType.TokenInfo? {
return DeeplinkType.TokenInfo(Mint(mint))
}

// https://app.flipcash.com/chat/{url encoded chatId}
// https://app.flipcash.com/chat/{url encoded e164}
private fun DeepLink.handleChat(): DeeplinkType.Chat? {
val uri = data.toUri()
val chatIdBase64 = uri.pathSegments.getOrNull(1) ?: return null
val chatId = ChatId(chatIdBase64.decodeBase64UrlSafe().toList())
return DeeplinkType.Chat(chatId)
// pathSegments already percent-decodes; do NOT urlDecode() again
// because URLDecoder treats '+' as a space, mangling phone numbers.
val chatTarget = uri.pathSegments.getOrNull(1) ?: return null

val identifier = if (chatTarget.startsWith("+")) {
ChatIdentifier.ByContact(DeviceContact.unknownContact(e164 = chatTarget))
} else {
val chatId = ChatId(chatTarget.decodeBase64UrlSafe().toList())
ChatIdentifier.ByChatId(chatId)
}

return DeeplinkType.Chat(identifier)
}

// https://app.flipcash.com/verify?email={email}&code={code}&client_data={data}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ message Navigation {

// Chat for the provided ID
common.v1.ChatId chat_id = 2;

// Chat for a contact with the provided phone number
phone.v1.PhoneNumber chat_contact_phone_number = 3;
}
}

Expand Down
54 changes: 54 additions & 0 deletions scripts/fcm-chat-phone.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/bin/bash
# Sends a chat push notification for a phone number contact.
# Usage: ./scripts/fcm-chat-phone.sh <device_token> <phone_number>
#
# Phone number must be E.164 format, e.g. +15551234567

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

DEVICE_TOKEN=$1
PHONE=$2

if [ -z "$DEVICE_TOKEN" ] || [ -z "$PHONE" ]; then
echo "Usage: $0 <device_token> <phone_number>"
echo " phone_number: E.164 format, e.g. +15551234567"
exit 1
fi

encode_payload() {
local phone="$1"
python3 -c "
import base64
def varint(v):
r = b''
while v > 0x7F:
r += bytes([(v & 0x7F) | 0x80]); v >>= 7
return r + bytes([v & 0x7F])
def field_bytes(num, data):
return varint((num << 3) | 2) + varint(len(data)) + data
def field_varint(num, val):
return varint((num << 3) | 0) + varint(val)
phone = b'$phone'
# Payload.navigation (field 1) > Navigation.chat_contact_phone_number (field 3) > PhoneNumber.value (field 1)
nav = field_bytes(1, field_bytes(3, field_bytes(1, phone)))
# Payload.category = CHAT (enum value 4)
cat = field_varint(4, 4)
# Payload.group_key
gk = field_bytes(5, b'chat_$phone')
print(base64.b64encode(nav + cat + gk).decode())
"
}

PAYLOAD=$(encode_payload "$PHONE")

JSON_PAYLOAD=$(jq -n \
--arg payload "$PAYLOAD" \
--arg title "Contact Joined!" \
--arg body "Tap to send them some cash" \
'{
flipcash_payload: $payload,
push_notification_title: $title,
push_notification_body: $body
}')

"$SCRIPT_DIR/fcm.sh" "$DEVICE_TOKEN" "$JSON_PAYLOAD"
2 changes: 1 addition & 1 deletion scripts/fcm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

source "$SCRIPT_DIR/../.env.local"
source "$SCRIPT_DIR/../.env"

ACCESS_TOKEN=$(python3 << EOF
from google.oauth2 import service_account
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ internal fun PushModels.Payload.asPayload(): NotificationPayload {
}
PushModels.Navigation.TypeCase.CHAT_ID -> {
val chatId = navigation.chatId.toChatId()
NavigationTrigger.Chat(chatId)
NavigationTrigger.Chat.ById(chatId)
}
PushModels.Navigation.TypeCase.CHAT_CONTACT_PHONE_NUMBER -> {
val phoneNumber = navigation.chatContactPhoneNumber.value
NavigationTrigger.Chat.ByContact(phoneNumber)
}
PushModels.Navigation.TypeCase.TYPE_NOT_SET -> null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,8 @@ data class NotificationPayload(

sealed interface NavigationTrigger {
data class CurrencyInfo(val mint: Mint) : NavigationTrigger
data class Chat(val chatId: ChatId) : NavigationTrigger
sealed interface Chat: NavigationTrigger {
data class ById(val chatId: ChatId) : Chat
data class ByContact(val phoneNumber: String) : Chat
}
}
Loading