Skip to content

Commit 242fe9e

Browse files
committed
feat: add rich push support
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent e0a3702 commit 242fe9e

15 files changed

Lines changed: 187 additions & 45 deletions

File tree

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/feed/ActivityFeedMessage.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ sealed interface MessageMetadata {
7373
@Serializable
7474
data object DepositedCrypto : MessageMetadata
7575

76+
@Serializable
77+
data object BoughtToken: MessageMetadata
78+
79+
@Serializable
80+
data object SoldToken: MessageMetadata
81+
7682
@Serializable
7783
data class PaidCrypto(
7884
val poolId: ID,
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package com.flipcash.app.core.util
22

33
import com.getcode.opencode.model.financial.Token
4+
import com.getcode.solana.keys.Mint
45
import com.getcode.solana.keys.base58
56
import com.getcode.utils.urlEncode
67

78
object Linkify {
89
fun cashLink(entropy: String): String = "https://send.flipcash.com/c/#/e=${entropy}"
910
fun download(shareRef: String): String = "https://flipcash.com/download?r=${shareRef}"
1011
fun tweet(message: String): String = "https://www.twitter.com/intent/tweet?text=${message.urlEncode()}"
11-
fun tokenInfo(token: Token): String = "https://app.flipcash.com/token/${token.address.base58()}"
12+
fun tokenInfo(token: Token): String = tokenInfo(token.address)
13+
fun tokenInfo(mint: Mint): String = "https://app.flipcash.com/token/${mint.base58()}"
1214
}

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

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@ import androidx.core.app.ActivityCompat
1414
import androidx.core.app.NotificationChannelCompat
1515
import androidx.core.app.NotificationCompat
1616
import androidx.core.app.NotificationManagerCompat
17+
import androidx.core.net.toUri
1718
import com.flipcash.app.auth.AuthManager
19+
import com.flipcash.app.core.util.Linkify
1820
import com.flipcash.services.controllers.PushController
21+
import com.flipcash.services.models.NavigationTrigger
22+
import com.flipcash.services.models.NotificationPayload
1923
import com.flipcash.services.user.UserManager
2024
import com.flipcash.shared.notifications.R
2125
import com.getcode.opencode.controllers.TokenController
@@ -31,7 +35,8 @@ import java.security.SecureRandom
3135
import javax.inject.Inject
3236

3337
@AndroidEntryPoint
34-
class NotificationService: FirebaseMessagingService(), CoroutineScope by CoroutineScope(Dispatchers.IO) {
38+
class NotificationService : FirebaseMessagingService(),
39+
CoroutineScope by CoroutineScope(Dispatchers.IO) {
3540

3641
@Inject
3742
lateinit var authManager: AuthManager
@@ -65,40 +70,57 @@ class NotificationService: FirebaseMessagingService(), CoroutineScope by Corouti
6570

6671
override fun onMessageReceived(message: RemoteMessage) {
6772
super.onMessageReceived(message)
68-
message.notification?.let { notification ->
69-
// dump everything into FCM fallback channel for now
70-
val channel = NotificationChannelCompat.Builder(
71-
"fcm_fallback_notification_channel",
72-
NotificationManagerCompat.IMPORTANCE_DEFAULT
73-
).setName("Misc.").build()
74-
75-
notificationManager.createNotificationChannel(channel)
76-
77-
val notificationBuilder: NotificationCompat.Builder =
78-
NotificationCompat.Builder(this, channel.id)
79-
.setPriority(NotificationCompat.PRIORITY_HIGH)
80-
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
81-
.setSmallIcon(R.drawable.flipcash_logo)
82-
.setColor(getColor(R.color.notification_color))
83-
.setAutoCancel(true)
84-
.setContentTitle(notification.title)
85-
.setContentText(notification.body)
86-
.setContentIntent(buildContentIntent())
87-
88-
val random = SecureRandom()
89-
val notificationId = random.nextInt(256)
9073

91-
launch {
92-
tokenController.update()
74+
// dump everything into FCM fallback channel for now
75+
val channel = NotificationChannelCompat.Builder(
76+
"fcm_fallback_notification_channel",
77+
NotificationManagerCompat.IMPORTANCE_DEFAULT
78+
).setName("Misc.").build()
79+
80+
val title = message.data["push_notification_title"]?.ifEmpty { message.notification?.title }
81+
val body = message.data["push_notification_body"]?.ifEmpty { message.notification?.body }
82+
83+
trace(
84+
message = "onMessageReceived",
85+
type = TraceType.Process,
86+
metadata = {
87+
"title" to title
88+
"body" to body
9389
}
90+
)
9491

95-
if (ActivityCompat.checkSelfPermission(
96-
this,
97-
Manifest.permission.POST_NOTIFICATIONS
98-
) == PackageManager.PERMISSION_GRANTED
99-
) {
100-
notificationManager.notify(notificationId, notificationBuilder.build())
92+
if (title == null) {
93+
return
94+
}
95+
96+
val payload = message.data.getOrDefault("flipcash_payload", "")
97+
.takeIf { it.isNotEmpty() }
98+
?.let { protoString ->
99+
NotificationPayload.fromEncoded(protoString)
101100
}
101+
102+
notificationManager.createNotificationChannel(channel)
103+
104+
val notificationBuilder: NotificationCompat.Builder =
105+
NotificationCompat.Builder(this, channel.id)
106+
.setPriority(NotificationCompat.PRIORITY_HIGH)
107+
.setSound(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION))
108+
.setSmallIcon(R.drawable.flipcash_logo)
109+
.setColor(getColor(R.color.notification_color))
110+
.setAutoCancel(true)
111+
.setContentTitle(title)
112+
.setContentText(body)
113+
.setContentIntent(buildContentIntent(payload?.navigation))
114+
115+
val random = SecureRandom()
116+
val notificationId = random.nextInt(256)
117+
118+
if (ActivityCompat.checkSelfPermission(
119+
this,
120+
Manifest.permission.POST_NOTIFICATIONS
121+
) == PackageManager.PERMISSION_GRANTED
122+
) {
123+
notificationManager.notify(notificationId, notificationBuilder.build())
102124
}
103125
}
104126

@@ -110,11 +132,19 @@ class NotificationService: FirebaseMessagingService(), CoroutineScope by Corouti
110132
}
111133
}
112134

113-
internal fun Context.buildContentIntent(): PendingIntent {
135+
internal fun Context.buildContentIntent(navigation: NavigationTrigger?): PendingIntent {
136+
val target = when (navigation) {
137+
is NavigationTrigger.CurrencyInfo -> Intent(Intent.ACTION_VIEW).apply {
138+
data = Linkify.tokenInfo(navigation.mint).toUri()
139+
}
140+
141+
else -> packageManager.getLaunchIntentForPackage(packageName)
142+
}
143+
114144
return PendingIntent.getActivity(
115145
this,
116146
99,
117-
packageManager.getLaunchIntentForPackage(packageName),
147+
target,
118148
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
119149
)
120150
}

apps/flipcash/shared/persistence/sources/src/main/kotlin/com/flipcash/app/persistence/sources/mapper/notifications/NotificationToEntityMapper.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ class MetadataMapper @Inject constructor(): Mapper<NotificationMetadata?, Messag
6262
NotificationMetadata.WelcomeBonus -> MessageMetadata.WelcomeBonus
6363
NotificationMetadata.WithdrewCrypto -> MessageMetadata.WithdrewCrypto
6464
NotificationMetadata.DepositedCrypto -> MessageMetadata.DepositedCrypto
65+
NotificationMetadata.BoughtToken -> MessageMetadata.BoughtToken
66+
NotificationMetadata.SoldToken -> MessageMetadata.SoldToken
6567
}
6668
}
6769
}

apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/RealSessionController.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,29 @@ import kotlin.time.Clock
8181
import kotlin.time.Duration.Companion.milliseconds
8282
import kotlin.time.Duration.Companion.seconds
8383

84+
/**
85+
* This class is the central orchestrator for the application's session, managing the user's
86+
* interaction flow, data synchronization, and state transitions. It integrates various
87+
* controllers and services to provide a cohesive user experience.
88+
*
89+
* Key Responsibilities:
90+
* - **State Management**: Maintains and exposes the overall session state ([SessionState]) and bill state
91+
* ([BillState]), reacting to changes in authentication, network connectivity, and app lifecycle.
92+
* - **Lifecycle Events**: Handles `onAppInForeground` and `onAppInBackground` events to start/stop
93+
* data polling (balances, activity feed), connect/disconnect services, and manage UI state.
94+
* - **Transaction Flow**:
95+
* - **Scanning**: Processes scanned QR codes ([onCodeScan]) to initiate cash grabs.
96+
* - **Cash Links**: Manages the creation ([shareGiftCard]) and claiming ([openCashLink]) of cash links.
97+
* - **Bill Presentation**: Coordinates with [BillController] to display bills for sending or
98+
* receiving cash ([showBill], [presentBillToUser]).
99+
* - **Data Synchronization**: Uses updaters ([TokenUpdater], [ActivityFeedUpdater], [ProfileUpdater])
100+
* to keep local data fresh.
101+
* - **User Interaction**: Manages UI feedback like toasts ([ToastController]), vibrations ([Vibrator]),
102+
* and bottom bar messages ([BottomBarManager]). It also handles interactions with the native
103+
* share sheet ([ShareSheetController]).
104+
* - **Feature & Settings Integration**: Responds to changes in feature flags and app settings,
105+
* such as `VibrateOnScan` and `CameraStartByDefault`.
106+
*/
84107
class RealSessionController @Inject constructor(
85108
private val billController: BillController,
86109
private val userManager: UserManager,

definitions/flipcash/protos/src/main/proto/activity/v1/model.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ message Notification {
3030
WithdrewCryptoNotificationMetadata withdrew_crypto = 9;
3131
SentCryptoNotificationMetadata sent_crypto = 10;
3232
DepositedCryptoNotificationMetadata deposited_crypto = 11;
33+
BoughtCryptoNotificationMetadata bought_crypto = 12;
34+
SoldCryptoNotificationMetadata sold_crypto = 13;
3335
}
3436
}
3537
message WelcomeBonusNotificationMetadata {
@@ -48,6 +50,10 @@ message SentCryptoNotificationMetadata {
4850
}
4951
message DepositedCryptoNotificationMetadata {
5052
}
53+
message BoughtCryptoNotificationMetadata {
54+
}
55+
message SoldCryptoNotificationMetadata {
56+
}
5157
// ActivityFeedType enables multiple activity feeds, where notifications may be
5258
// split across different parts of the app
5359
enum ActivityFeedType {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
syntax = "proto3";
2+
package flipcash.push.v1;
3+
option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/go/push/v1;pushpb";
4+
option java_package = "com.codeinc.flipcash.gen.push.v1";
5+
option objc_class_prefix = "FPBPushV1";
6+
import "common/v1/common.proto";
7+
8+
enum TokenType {
9+
UNKNOWN = 0;
10+
// FCM registration token for an Android device
11+
FCM_ANDROID = 1;
12+
// FCM registration token or an iOS device
13+
FCM_APNS = 2;
14+
}
15+
// Payload provided as extra data in a push
16+
message Payload {
17+
// If present, where the app should navigate to after clicking the push
18+
Navigation navigation = 1;
19+
}
20+
// Navigation within the app upon clicking the push
21+
message Navigation {
22+
oneof type {
23+
// Currency info page for the provided mint
24+
common.v1.PublicKey currency_info = 1;
25+
}
26+
}

definitions/flipcash/protos/src/main/proto/push/v1/push_service.proto

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,14 @@ option go_package = "github.com/code-payments/flipcash2-protobuf-api/generated/g
44
option java_package = "com.codeinc.flipcash.gen.push.v1";
55
option objc_class_prefix = "FPBPushV1";
66
import "common/v1/common.proto";
7+
import "push/v1/model.proto";
78

89
service Push {
910
// AddToken adds a push token associated with a user.
1011
rpc AddToken(AddTokenRequest) returns (AddTokenResponse);
1112
// DeleteTokens removes all push tokens within an app install for a user
1213
rpc DeleteTokens(DeleteTokensRequest) returns (DeleteTokensResponse);
1314
}
14-
enum TokenType {
15-
UNKNOWN = 0;
16-
// FCM registration token for an Android device
17-
FCM_ANDROID = 1;
18-
// FCM registration token or an iOS device
19-
FCM_APNS = 2;
20-
}
2115
message AddTokenRequest {
2216
TokenType token_type = 1;
2317
string push_token = 2 ;

services/flipcash/src/main/kotlin/com/flipcash/services/internal/domain/ActivityFeedMessageMapper.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ internal class ActivityFeedMessageMapper @Inject constructor(
6969
canCancel = from.sentCrypto.canInitiateCancelAction
7070
)
7171
Model.Notification.AdditionalMetadataCase.DEPOSITED_CRYPTO -> NotificationMetadata.DepositedCrypto
72+
Model.Notification.AdditionalMetadataCase.BOUGHT_CRYPTO -> NotificationMetadata.BoughtToken
73+
Model.Notification.AdditionalMetadataCase.SOLD_CRYPTO -> NotificationMetadata.SoldToken
7274
Model.Notification.AdditionalMetadataCase.ADDITIONALMETADATA_NOT_SET,
7375
null -> NotificationMetadata.Unknown
7476
}

services/flipcash/src/main/kotlin/com/flipcash/services/internal/network/api/PushApi.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.flipcash.services.internal.network.api
22

33
import com.codeinc.flipcash.gen.common.v1.Common
4+
import com.codeinc.flipcash.gen.push.v1.Model
45
import com.codeinc.flipcash.gen.push.v1.PushGrpcKt
56
import com.codeinc.flipcash.gen.push.v1.PushService
67
import com.flipcash.services.internal.annotations.FlipcashManagedChannel
@@ -34,7 +35,7 @@ internal class PushApi @Inject constructor(
3435
PushService.AddTokenRequest.newBuilder()
3536
.setPushToken(token)
3637
.setAppInstall(Common.AppInstallId.newBuilder().setValue(installationId))
37-
.setTokenType(PushService.TokenType.FCM_ANDROID)
38+
.setTokenType(Model.TokenType.FCM_ANDROID)
3839
.apply { setAuth(authenticate(owner)) }
3940
.build()
4041

0 commit comments

Comments
 (0)