Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SDK 4.0 - Push Notifications #593

Merged
merged 46 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
5bac363
✨ Adding _device property on session_started events
andretortolano Feb 14, 2024
5af6d06
✨ Adding Appcues.setPushToken method
andretortolano Feb 22, 2024
b9d5dda
✨ Adding _pushToken property + debugger visual
andretortolano Feb 22, 2024
bb3a81e
✨ Adding device updated event
andretortolano Feb 22, 2024
b17e4d2
✨ Adding device unregistered event
andretortolano Feb 23, 2024
834d125
✨ Adding _pushEnabled property
andretortolano Feb 29, 2024
26ee3e6
✨ Adding _pushPrimerEligible property
andretortolano Mar 6, 2024
451edf5
✨ Adding _pushEnabledBackground property
andretortolano Mar 6, 2024
9cd8360
✨ Adding AppcuesFirebaseMessagingService
andretortolano Mar 15, 2024
f75545f
✨ Adding happy path for verifying that notification is working properly
andretortolano Mar 20, 2024
20684ac
♻️ data from jsonFormat + improving channel setup
andretortolano Mar 25, 2024
3aef836
👌 Fixing Push validation through debugger
andretortolano Apr 1, 2024
a3fe830
👌 checking for preview vs test push
andretortolano Apr 2, 2024
45b968f
♻️ Improving how debugger status item handle retries
andretortolano Apr 2, 2024
4b181e3
👌 adjusting push opened event propertiess
andretortolano Apr 2, 2024
c3743ec
✨ Adding request-push action
andretortolano Apr 1, 2024
3c5eaf2
👌 Adding safe-guard to request permission activity
andretortolano Apr 3, 2024
7927064
♻️ Updating sample app to support show experience
andretortolano Apr 4, 2024
5368d74
✨ Adding shortcut for settings when Push notification permission is n…
andretortolano Apr 5, 2024
9b82b54
♻️ Adding properties to push data & improve naming
andretortolano Apr 5, 2024
1c92366
✨ Adding push_preview and push_content deeplinks
andretortolano Apr 9, 2024
f66720b
♻️ Storing deferred actions when processing notification deeplink in…
andretortolano Apr 12, 2024
4869247
♻️ Processing deferred notification actions during identify call
andretortolano Apr 15, 2024
8fa84bb
♻️ Improving debugger push check
andretortolano Apr 16, 2024
22eae12
♻️ Democues adjustments (schemes) and firebaseToken
andretortolano Apr 18, 2024
0f22107
🐛 Fix Push Opened Action for preview
andretortolano Apr 22, 2024
a9db312
🐛 Fix DeeplinkHandlerTest
andretortolano Apr 29, 2024
c722ff9
♻️ Update release script for sdk4 branch
andretortolano Apr 29, 2024
0f90e29
♻️ Auto collecting push token whenever appcues is instanciated
andretortolano Apr 29, 2024
819d352
🔖 Update version to 4.0.0-alpha1
andretortolano May 1, 2024
26be629
📈 Add reason to device unregistered analytic event
iujames May 1, 2024
f80a6d6
♻️ Revise automatic push token collection
iujames May 2, 2024
677779b
📈 Track device updated events on re-identify
iujames May 1, 2024
f2f9d40
♻️ Adding new push verification check for the manifest.xml
andretortolano May 2, 2024
fe84754
♻️ Enhancing preview feedback on backend error
andretortolano May 6, 2024
8869977
✨ Adding google services key (protected key)
andretortolano May 10, 2024
204e14d
✨ Check for push enabled status changes while app is not active
iujames May 24, 2024
b62c527
♻️ Check for push token set for push enabled state
iujames May 24, 2024
e06c5a4
🔖 Update version to 4.0.0-alpha2
andretortolano Jun 24, 2024
d928c9f
✨ Adding trigger information analytics
andretortolano Jul 12, 2024
0c2b02d
♻️ Reorganizing ExperienceLifecycleEvent to improve readability
andretortolano Jul 15, 2024
d7c8c00
📝 push notification docs and update example app
andretortolano Jul 22, 2024
0a66c35
💥 Removing deprecated Anonymous call
andretortolano Jul 24, 2024
251aa1f
🔖 Update version to 4.0.0-beta01
iujames Jul 24, 2024
b172577
♻️ Update property name to match iOS report
andretortolano Sep 13, 2024
ecd3c96
♻️ SDK4 branch merge cleanup
andretortolano Sep 16, 2024
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ output.json
/.idea/deploymentTargetDropDown.xml
/.idea/assetWizardSettings.xml
/.idea/kotlinc.xml
/.idea/deploymentTargetSelector.xml
/.idea/other.xml
vendor/

# Keystore files
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The SDK is a Kotlin library for sending user properties and events to the Appcue
- [One Time Setup](#one-time-setup)
- [Initializing the SDK](#initializing-the-sdk)
- [Supporting Builder Preview and Screen Capture](#supporting-builder-preview-and-screen-capture)
- [Enabling Push Notifications](#enabling-push-notifications)
- [Identifying Users](#identifying-users)
- [Tracking Screens and Events](#tracking-screens-and-events)
- [Anchored Tooltips](#anchored-tooltips)
Expand Down Expand Up @@ -99,6 +100,10 @@ Initializing the SDK requires you to provide two values: `APPCUES_ACCOUNT_ID` an

During installation, follow the steps outlined in [Configuring the Appcues URL Scheme](https://github.com/appcues/appcues-android-sdk/blob/main/docs/URLSchemeConfiguring.md). This is necessary for the complete Appcues builder experience, supporting experience preview, screen capture and debugging. Refer to the [Debug Guide](https://github.com/appcues/appcues-android-sdk/blob/main/docs/Debugging.md) for details about using the Appcues debugger.

#### Enabling Push Notifications

During installation, follow the steps outlined in [Push Notification](https://github.com/appcues/appcues-android-sdk/blob/main/docs/PushNotification.md).

### Identifying Users

In order to target content to the right users at the right time, you need to identify users and send Appcues data about them. A user is identified with a unique ID.
Expand Down
8 changes: 4 additions & 4 deletions appcues/appcues.properties
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
GROUP_ID = com.appcues
ARTIFACT_ID = appcues
VERSION_MAJOR = 3
VERSION_MINOR = 3
VERSION_PATCH = 2
VERSION_CLASSIFIER =
VERSION_MAJOR = 4
VERSION_MINOR = 0
VERSION_PATCH = 0
VERSION_CLASSIFIER = beta01
NAME = Appcues
DESCRIPTION = Kotlin SDK for embedding Appcues experiences in Android applications.
ORG_ID = appcues
Expand Down
2 changes: 2 additions & 0 deletions appcues/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ dependencies {
// Play In-App Review
implementation 'com.google.android.play:review:2.0.1'
implementation 'com.google.android.play:review-ktx:2.0.1'
// Messaging
implementation "com.google.firebase:firebase-messaging-ktx:23.4.1"
// Animation
implementation 'nl.dionsegijn:konfetti-compose:2.0.4'

Expand Down
7 changes: 5 additions & 2 deletions appcues/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,23 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />

<uses-permission android:name="android.permission.VIBRATE" />

<application>
<activity
android:name=".ui.InAppReviewActivity"
android:exported="false"
android:theme="@style/Appcues.AppcuesActivityTheme" />
<activity
android:name=".ui.RequestPermissionActivity"
android:exported="false"
android:theme="@style/Appcues.AppcuesActivityTheme" />

<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">

<meta-data
android:name="com.appcues.monitor.AppcuesInitializer"
android:value="androidx.startup" />
Expand Down
86 changes: 70 additions & 16 deletions appcues/src/main/java/com/appcues/Appcues.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.appcues.action.ActionRegistry
import com.appcues.action.ExperienceAction
import com.appcues.analytics.ActivityScreenTracking
import com.appcues.analytics.AnalyticsEvent
import com.appcues.analytics.AnalyticsTracker
import com.appcues.data.model.ExperienceTrigger
import com.appcues.data.model.RenderContext
Expand All @@ -18,12 +19,13 @@
import com.appcues.di.scope.inject
import com.appcues.logging.LogcatDestination
import com.appcues.logging.Logcues
import com.appcues.push.PushOpenedProcessor
import com.appcues.trait.ExperienceTrait
import com.appcues.trait.ExperienceTraitLevel
import com.appcues.trait.TraitRegistry
import com.appcues.ui.ExperienceRenderer
import com.google.firebase.messaging.FirebaseMessaging
import kotlinx.coroutines.launch
import kotlin.DeprecationLevel.ERROR

/**
* Construct and return an instance of the Appcues SDK.
Expand All @@ -38,13 +40,40 @@
accountId: String,
applicationId: String,
config: (AppcuesConfig.() -> Unit)? = null,
): Appcues = Bootstrap
): Appcues {
// This creates the Scope and initializes the Appcues instance within, then returns the Appcues instance
// ready to go with the necessary dependency configuration in its scope.
.createScope(
context = context,
config = AppcuesConfig(accountId, applicationId).apply { config?.invoke(this) }
).get()
val scope = Bootstrap
.createScope(
context = context,
config = AppcuesConfig(accountId, applicationId).apply { config?.invoke(this) }
)

val appcues = scope.get<Appcues>()

try {
// this is necessary to ensure that not only when we get a new token but whenever we initialize the application appcues should know
// whats the latest available token
FirebaseMessaging.getInstance().token.addOnCompleteListener {

Check warning on line 57 in appcues/src/main/java/com/appcues/Appcues.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/Appcues.kt#L57

Added line #L57 was not covered by tests
// ensures we have token set
if (it.isSuccessful) {
val sessionMonitor = scope.get<SessionMonitor>()

Check warning on line 60 in appcues/src/main/java/com/appcues/Appcues.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/Appcues.kt#L60

Added line #L60 was not covered by tests
if (sessionMonitor.hasSession()) {
// can call setPushToken directly if we are already in a session, so the device_updated event is tracked
appcues.setPushToken(it.result)

Check warning on line 63 in appcues/src/main/java/com/appcues/Appcues.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/Appcues.kt#L63

Added line #L63 was not covered by tests
} else {
// store token on static pushToken that will be used on the next session start to track device props
val storage = scope.get<Storage>()
storage.pushToken = it.result

Check warning on line 67 in appcues/src/main/java/com/appcues/Appcues.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/Appcues.kt#L66-L67

Added lines #L66 - L67 were not covered by tests
}
}
}

Check warning on line 70 in appcues/src/main/java/com/appcues/Appcues.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/Appcues.kt#L70

Added line #L70 was not covered by tests
} catch (_: Exception) {
// do nothing on any exception we may hit here as customer might not even have google messaging services setup
}

return appcues
}

/**
* The main entry point for using Appcues functionality in your application - tracking
Expand All @@ -66,6 +95,8 @@
* provided by the SDK is based on Android View layout information.
*/
public var elementTargeting: ElementTargetingStrategy = AndroidTargetingStrategy()

internal var pushToken: String? = null
}

private val config by scope.inject<AppcuesConfig>()
Expand All @@ -81,6 +112,7 @@
private val debuggerManager by scope.inject<AppcuesDebuggerManager>()
private val appcuesCoroutineScope by scope.inject<AppcuesCoroutineScope>()
private val analyticsPublisher by scope.inject<AnalyticsPublisher>()
private val pushOpenedProcessor by scope.inject<PushOpenedProcessor>()

/**
* Set the listener to be notified about the display of Experience content.
Expand Down Expand Up @@ -159,21 +191,17 @@
identify(true, "anon:$anonymousId", null)
}

/**
* This function has been removed. Calling the anonymous function with a properties parameter
* is no longer supported. A call to `anonymous()` with no parameters should be used instead.
*/
@Deprecated("properties are no longer supported for anonymous users.", level = ERROR)
@Suppress("UnusedPrivateMember", "UNUSED_PARAMETER")
public fun anonymous(properties: Map<String, Any>?) {
// removed
}

/**
* Clears out the current user in this session.
* Can be used when the user logs out of your application.
*/
public fun reset() {
analyticsTracker.track(
name = AnalyticsEvent.DeviceUnregistered.eventName,
properties = mapOf("reason" to "sdk_reset"),
isInternal = true,
interactive = false
)
// flush any pending analytics for the previous user, prior to reset
analyticsTracker.flushPendingActivity()

Expand Down Expand Up @@ -263,6 +291,19 @@
}
}

/**
* Provide the Firebase Cloud Messaging (FCM) device token to Appcues.
*
* @param token A globally unique token that identifies this device to FCM.
*/
public fun setPushToken(token: String?) {
if (token != storage.pushToken) {
storage.pushToken = token

Check warning on line 301 in appcues/src/main/java/com/appcues/Appcues.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/Appcues.kt#L301

Added line #L301 was not covered by tests

analyticsTracker.track(AnalyticsEvent.DeviceUpdated.eventName, properties = null, interactive = true, isInternal = true)

Check warning on line 303 in appcues/src/main/java/com/appcues/Appcues.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/Appcues.kt#L303

Added line #L303 was not covered by tests
}
}

Check warning on line 305 in appcues/src/main/java/com/appcues/Appcues.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/Appcues.kt#L305

Added line #L305 was not covered by tests

/**
* Enables automatic screen tracking for Activities.
*/
Expand Down Expand Up @@ -322,5 +363,18 @@
storage.isAnonymous = isAnonymous
storage.userSignature = mutableProperties?.remove("appcues:user_id_signature") as? String
analyticsTracker.identify(mutableProperties)
if (!userChanged) {
// track a device update on any re-identify of the same user as well, since it will not trigger a new
// session but this is a way to force an update of any device props that may have changed outside of the SDK
// i.e. push permission.
// this is interactive=true so it gets batched together with the identify in a single request
analyticsTracker.track(AnalyticsEvent.DeviceUpdated.eventName, properties = null, interactive = true, isInternal = true)
}

// whenever user identifies we check to see if there is a pending push open action matching the userId,
// in case there is we run it.
appcuesCoroutineScope.launch {
pushOpenedProcessor.processDeferred(userId)
}
}
}
122 changes: 122 additions & 0 deletions appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.appcues

import android.Manifest.permission
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import com.appcues.data.MoshiConfiguration
import com.appcues.util.getNotificationBuilder
import com.appcues.util.notify
import com.appcues.util.setContent
import com.appcues.util.setIntent
import com.appcues.util.setStyle
import com.appcues.util.setupNotification
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

/**
* Appcues Firebase Messaging Service implementation.
*
* To use it you can either add directly to your Manifest as a service or
* call handleMessage and setToken on your custom implementation.
*/
public class AppcuesFirebaseMessagingService : FirebaseMessagingService() {

Check warning on line 25 in appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt#L25

Added line #L25 was not covered by tests

public companion object {

internal var notificationId = 1_000_000

Check warning on line 29 in appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt#L29

Added line #L29 was not covered by tests

internal const val CHECK_PUSH_NOTIFICATION_ID = 1_100_100

/**
* handleMessage will try to parse the received message into an Appcues notification, if it does it will return true,
* or false in case the message is not for Appcues to handle
*
* @param context application context ('this' when calling from service)
* @param message remote message coming from the messaging service
*
* @return determining whether we handled the message or not.
*/
@JvmStatic
public fun handleMessage(context: Context, message: RemoteMessage): Boolean {
val data = try {

Check warning on line 44 in appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt#L44

Added line #L44 was not covered by tests
val appcuesData = message.data["appcues"] ?: throw IllegalStateException("Appcues message data not found.")
MoshiConfiguration.moshi.adapter(AppcuesMessagingData::class.java).fromJson(appcuesData)!!
} catch (_: Exception) {

Check warning on line 47 in appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt#L46-L47

Added lines #L46 - L47 were not covered by tests
// unable to get data, most likely means this message is not for us.
return false

Check warning on line 49 in appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt#L49

Added line #L49 was not covered by tests
}

// check for permission
if (ActivityCompat.checkSelfPermission(context, permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED)
return false

Check warning on line 54 in appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt#L54

Added line #L54 was not covered by tests

val isCheckPush = data.test && data.notificationId.startsWith("test-push")

context.getNotificationBuilder()
.setupNotification(message)
.setContent(data)
.setStyle(context, data)
.setIntent(context, data, isCheckPush)
.notify(context, isCheckPush)

Check warning on line 63 in appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt#L58-L63

Added lines #L58 - L63 were not covered by tests

return true

Check warning on line 65 in appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt#L65

Added line #L65 was not covered by tests
}

/**
* sets the global appcues token
*
* @param token token value coming from the messaging service
*/
@JvmStatic
public fun setToken(token: String) {
Appcues.pushToken = token
}

Check warning on line 76 in appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt#L75-L76

Added lines #L75 - L76 were not covered by tests
}

@JsonClass(generateAdapter = true)
internal data class AppcuesMessagingData(
val title: String,
val body: String,
@Json(name = "notification_id")
val notificationId: String,
@Json(name = "notification_version")
val notificationVersion: Long? = null,
@Json(name = "account_id")
val accountId: String,
@Json(name = "app_id")
val appId: String,
@Json(name = "user_id")
val userId: String,
@Json(name = "workflow_id")
val workflowId: String?,
@Json(name = "workflow_task_id")
val workflowTaskId: String?,
@Json(name = "workflow_version")
val workflowVersion: Long? = null,
@Json(name = "deep_link_url")
val deeplink: String?,
@Json(name = "attachment_url")
val image: String?,
@Json(name = "experience_id")
val experienceId: String?,
@Json(name = "category")
val category: String?,
@Json(name = "android_notification_id")
val androidNotificationId: String?,
@Json(name = "test")
val test: Boolean = false
)

Check warning on line 111 in appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt#L79-L111

Added lines #L79 - L111 were not covered by tests

override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
handleMessage(this, remoteMessage)
}

Check warning on line 116 in appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt#L114-L116

Added lines #L114 - L116 were not covered by tests

override fun onNewToken(token: String) {
super.onNewToken(token)
setToken(token)
}

Check warning on line 121 in appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt

View check run for this annotation

Codecov / codecov/patch

appcues/src/main/java/com/appcues/AppcuesFirebaseMessagingService.kt#L119-L121

Added lines #L119 - L121 were not covered by tests
}
Loading