Skip to content

Commit 3863852

Browse files
authored
feat(notifications): resolve CONTACT_JOIN push substitutions to contact names (#805)
Map proto substitution fields into NotificationPayload, resolve phone numbers to local contact display names via ContactDataSource (falling back to formatted number, then the server fallback string), and apply indexed {0}/{1}/… replacements to push title and body. Also refactors NotificationService: extracts postNotification(), moves data keys to companion consts. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 219f3cc commit 3863852

4 files changed

Lines changed: 91 additions & 38 deletions

File tree

apps/flipcash/shared/notifications/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ android {
99

1010
dependencies {
1111
implementation(project(":apps:flipcash:shared:authentication"))
12+
implementation(project(":apps:flipcash:shared:persistence:sources"))
13+
implementation(project(":apps:flipcash:shared:phone"))
1214
implementation(project(":apps:flipcash:shared:push"))
1315
implementation(project(":apps:flipcash:shared:tokens"))
1416
implementation(project(":services:flipcash"))

apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/NotificationService.kt

Lines changed: 68 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ import androidx.core.app.NotificationManagerCompat
1212
import androidx.core.net.toUri
1313
import com.flipcash.app.auth.AuthManager
1414
import com.flipcash.app.core.util.Linkify
15+
import com.flipcash.app.persistence.sources.ContactDataSource
16+
import com.flipcash.app.phone.PhoneUtils
1517
import com.flipcash.app.tokens.TokenCoordinator
1618
import com.flipcash.services.controllers.PushController
1719
import com.flipcash.services.models.NavigationTrigger
1820
import com.flipcash.services.models.NotificationCategory
1921
import com.flipcash.services.models.NotificationPayload
22+
import com.flipcash.services.models.Substitution
2023
import com.flipcash.services.user.UserManager
2124
import com.flipcash.shared.notifications.R
2225
import com.getcode.utils.TraceType
@@ -34,6 +37,12 @@ import javax.inject.Inject
3437
class NotificationService : FirebaseMessagingService(),
3538
CoroutineScope by CoroutineScope(Dispatchers.IO) {
3639

40+
companion object {
41+
private const val KEY_TITLE = "push_notification_title"
42+
private const val KEY_BODY = "push_notification_body"
43+
private const val KEY_PAYLOAD = "flipcash_payload"
44+
}
45+
3746
@Inject
3847
lateinit var authManager: AuthManager
3948

@@ -49,6 +58,12 @@ class NotificationService : FirebaseMessagingService(),
4958
@Inject
5059
lateinit var tokenCoordinator: TokenCoordinator
5160

61+
@Inject
62+
lateinit var contactDataSource: ContactDataSource
63+
64+
@Inject
65+
lateinit var phoneUtils: PhoneUtils
66+
5267
override fun onNewToken(token: String) {
5368
super.onNewToken(token)
5469
authenticateIfNeeded {
@@ -69,8 +84,8 @@ class NotificationService : FirebaseMessagingService(),
6984
override fun onMessageReceived(message: RemoteMessage) {
7085
super.onMessageReceived(message)
7186

72-
val title = message.data["push_notification_title"]?.ifEmpty { message.notification?.title }
73-
val body = message.data["push_notification_body"]?.ifEmpty { message.notification?.body }
87+
val title = message.data[KEY_TITLE]?.ifEmpty { message.notification?.title }
88+
val body = message.data[KEY_BODY]?.ifEmpty { message.notification?.body }
7489

7590
trace(
7691
message = "onMessageReceived",
@@ -81,61 +96,76 @@ class NotificationService : FirebaseMessagingService(),
8196
}
8297
)
8398

84-
if (title == null) {
85-
return
86-
}
99+
if (title == null) return
87100

88-
val payload = message.data.getOrDefault("flipcash_payload", "")
101+
val payload = message.data.getOrDefault(KEY_PAYLOAD, "")
89102
.takeIf { it.isNotEmpty() }
90-
?.let { protoString ->
91-
NotificationPayload.fromEncoded(protoString)
92-
}
103+
?.let { NotificationPayload.fromEncoded(it) }
93104

94105
if (payload?.navigation is NavigationTrigger.CurrencyInfo) {
95-
launch {
96-
tokenCoordinator.update()
97-
}
106+
launch { tokenCoordinator.update() }
98107
}
99108

109+
launch {
110+
val resolvedTitle = applySubstitutions(title, payload?.titleSubstitutions.orEmpty())
111+
val resolvedBody = body?.let { applySubstitutions(it, payload?.bodySubstitutions.orEmpty()) }
112+
postNotification(resolvedTitle, resolvedBody, payload)
113+
}
114+
}
115+
116+
private fun postNotification(title: String, body: String?, payload: NotificationPayload?) {
100117
val category = payload?.category ?: NotificationCategory.DEFAULT
101118
NotificationChannels.ensureChannelGroups(this, notificationManager)
102119
val channel = NotificationChannels.channelFor(this, category)
103120
notificationManager.createNotificationChannel(channel)
104121

105122
val groupKey = payload?.groupKey?.takeIf { it.isNotEmpty() }
106123

107-
val notificationBuilder: NotificationCompat.Builder =
108-
NotificationCompat.Builder(this, channel.id)
109-
.setPriority(NotificationCompat.PRIORITY_HIGH)
110-
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
124+
val notification = NotificationCompat.Builder(this, channel.id)
125+
.setPriority(NotificationCompat.PRIORITY_HIGH)
126+
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
127+
.setSmallIcon(R.drawable.flipcash_logo)
128+
.setColor(getColor(R.color.notification_color))
129+
.setAutoCancel(true)
130+
.setContentTitle(title)
131+
.setContentText(body)
132+
.setContentIntent(buildContentIntent(payload?.navigation))
133+
.apply { if (groupKey != null) setGroup(groupKey) }
134+
.build()
135+
136+
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
137+
!= PackageManager.PERMISSION_GRANTED
138+
) return
139+
140+
val notificationId = SecureRandom().nextInt(Int.MAX_VALUE)
141+
notificationManager.notify(notificationId, notification)
142+
143+
if (groupKey != null) {
144+
val summary = NotificationCompat.Builder(this, channel.id)
111145
.setSmallIcon(R.drawable.flipcash_logo)
112146
.setColor(getColor(R.color.notification_color))
147+
.setGroup(groupKey)
148+
.setGroupSummary(true)
113149
.setAutoCancel(true)
114-
.setContentTitle(title)
115-
.setContentText(body)
116-
.setContentIntent(buildContentIntent(payload?.navigation))
117-
.apply { if (groupKey != null) setGroup(groupKey) }
150+
.build()
151+
notificationManager.notify(groupKey.hashCode(), summary)
152+
}
153+
}
118154

119-
val notificationId = SecureRandom().nextInt(Int.MAX_VALUE)
155+
private suspend fun resolveSubstitution(substitution: Substitution): String {
156+
val phoneNumber = substitution.phoneNumber ?: return substitution.fallback
157+
val displayName = contactDataSource.getDisplayName(phoneNumber)
158+
if (displayName != null) return displayName
159+
return runCatching { phoneUtils.formatNumber(phoneNumber) }.getOrDefault(substitution.fallback)
160+
}
120161

121-
if (ActivityCompat.checkSelfPermission(
122-
this,
123-
Manifest.permission.POST_NOTIFICATIONS
124-
) == PackageManager.PERMISSION_GRANTED
125-
) {
126-
notificationManager.notify(notificationId, notificationBuilder.build())
127-
128-
if (groupKey != null) {
129-
val summary = NotificationCompat.Builder(this, channel.id)
130-
.setSmallIcon(R.drawable.flipcash_logo)
131-
.setColor(getColor(R.color.notification_color))
132-
.setGroup(groupKey)
133-
.setGroupSummary(true)
134-
.setAutoCancel(true)
135-
.build()
136-
notificationManager.notify(groupKey.hashCode(), summary)
137-
}
162+
private suspend fun applySubstitutions(text: String, substitutions: List<Substitution>): String {
163+
var result = text
164+
for ((index, substitution) in substitutions.withIndex()) {
165+
val resolved = resolveSubstitution(substitution)
166+
result = result.replace("{$index}", resolved)
138167
}
168+
return result
139169
}
140170

141171
private fun authenticateIfNeeded(block: () -> Unit) {

services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/extensions/ProtobufToLocal.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.flipcash.services.internal.extensions.toPublicKey
1212
import com.flipcash.services.models.NavigationTrigger
1313
import com.flipcash.services.models.NotificationCategory
1414
import com.flipcash.services.models.NotificationPayload
15+
import com.flipcash.services.models.Substitution
1516
import com.getcode.opencode.model.core.ID
1617
import com.getcode.solana.keys.Checksum
1718
import com.getcode.solana.keys.Mint
@@ -45,13 +46,26 @@ internal fun PushModels.Payload.asPayload(): NotificationPayload {
4546
else -> NotificationCategory.DEFAULT
4647
}
4748

49+
val titleSubs = titleSubstitutionsList.map { it.asSubstitution() }
50+
val bodySubs = bodySubstitutionsList.map { it.asSubstitution() }
51+
4852
return NotificationPayload(
4953
navigation = navigationTrigger,
5054
category = notificationCategory,
5155
groupKey = groupKey,
56+
titleSubstitutions = titleSubs,
57+
bodySubstitutions = bodySubs,
5258
)
5359
}
5460

61+
internal fun PushModels.Substitution.asSubstitution(): Substitution {
62+
val phoneNumber = when (kindCase) {
63+
PushModels.Substitution.KindCase.CONTACT -> contact.value
64+
else -> null
65+
}
66+
return Substitution(fallback = fallback, phoneNumber = phoneNumber)
67+
}
68+
5569
internal fun Common.Signature.toSignature(): Signature {
5670
return Signature(value.toByteArray().toList())
5771
}

services/flipcash/src/main/kotlin/com/flipcash/services/models/NotificationPayload.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,17 @@ enum class NotificationCategory {
1414
CONTACT_JOIN,
1515
}
1616

17+
data class Substitution(
18+
val fallback: String,
19+
val phoneNumber: String?,
20+
)
21+
1722
data class NotificationPayload(
1823
val navigation: NavigationTrigger?,
1924
val category: NotificationCategory = NotificationCategory.DEFAULT,
2025
val groupKey: String = "",
26+
val titleSubstitutions: List<Substitution> = emptyList(),
27+
val bodySubstitutions: List<Substitution> = emptyList(),
2128
) {
2229
companion object {
2330
fun fromEncoded(encoded: String): NotificationPayload? {

0 commit comments

Comments
 (0)