@@ -12,11 +12,14 @@ import androidx.core.app.NotificationManagerCompat
1212import androidx.core.net.toUri
1313import com.flipcash.app.auth.AuthManager
1414import com.flipcash.app.core.util.Linkify
15+ import com.flipcash.app.persistence.sources.ContactDataSource
16+ import com.flipcash.app.phone.PhoneUtils
1517import com.flipcash.app.tokens.TokenCoordinator
1618import com.flipcash.services.controllers.PushController
1719import com.flipcash.services.models.NavigationTrigger
1820import com.flipcash.services.models.NotificationCategory
1921import com.flipcash.services.models.NotificationPayload
22+ import com.flipcash.services.models.Substitution
2023import com.flipcash.services.user.UserManager
2124import com.flipcash.shared.notifications.R
2225import com.getcode.utils.TraceType
@@ -34,6 +37,12 @@ import javax.inject.Inject
3437class 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 ) {
0 commit comments