Skip to content

Commit

Permalink
Updated Android sample app instructions (#153)
Browse files Browse the repository at this point in the history
* Reviewed sample app with an eye to android set up

* Got sample app running with firebase so you can actually use it to test, had to fix some dependencies and I consolidated where all the configs live at least for android specific

* Add note about opened push / RN initialization timing

* Conditional gradle config to allow compiling without a google-services file

---------

Co-authored-by: Evan Masseau <>
  • Loading branch information
evan-masseau committed Jun 6, 2024
1 parent 36c994a commit 3d10d55
Show file tree
Hide file tree
Showing 21 changed files with 336 additions and 58 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ project.xcworkspace
.settings
local.properties
android.iml
google-services.json

# Cocoapods
#
Expand Down
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,25 @@ yarn add klaviyo-react-native-sdk

### Example App

We have included a bare-bones example app in this repository for reference of how to integrate with our SDK.
It is primarily intended to give code samples such as how and where to `initialize` or how to implement notification
delegate methods on iOS. To actually run the example app:
We have included an example app in this repository for reference of how to integrate with our SDK.
It is primarily intended to give code samples such as how and where to `initialize`, implement notification
delegate methods on iOS, and handle an opened notification intent on Android. We've commented the sample app
code to call out key setup steps, search for `iOS Installation Step` and `Android Installation Step`.

To run the example app:

- Clone this repository
- From the root directory, run `yarn example-setup`. This is an alias that will do the following:
- Run `yarn install --immutable` from the root directory
- Navigate to the `example` directory and run `bundle install`
- Navigate to the `example/ios` directory and run `bundle exec pod install`
- Android configuration:
- To initialize Klaviyo from the native layer, open `example/android/gradle.properties` and follow the
instructions to set your `publicApiKey` and verify `initializeKlaviyoFromNative` is enabled.
- If you wish to run the Android example app with push/firebase, you'll need to copy a `google-services.json`
file into `example/android/app/src` and update the `applicationId` in `app/build.gradle` to match your application ID.
Then, open `example/android/gradle.properties` and follow the instructions to enable `useNativeFirebase`.
This is disabled by default because the app will crash on launch without a `google-services.json` file.
- From the project's root directory, run `yarn example start` to start the example application. Follow the
metro instructions from here, i.e. press `i` to run on iOS or `a` to run on Android.

Expand Down Expand Up @@ -151,7 +161,7 @@ Below is an example of how to initialize the SDK from your React Native code:

```typescript
import { Klaviyo } from 'klaviyo-react-native-sdk';
Klaviyo.initialize('YOUR_PUBLIC_KLAVIYO_API_KEY');
Klaviyo.initialize('YOUR_KLAVIYO_PUBLIC_API_KEY');
```

### Native Initialization
Expand Down Expand Up @@ -407,11 +417,17 @@ No additional setup is needed to support rich push on Android.
#### Tracking Open Events

Klaviyo tracks push opens events with a specially formatted event `Opened Push` that includes message tracking
parameters in the event properties. To track push opens, you will need to follow platform-specific instructions:
parameters in the event properties. To track push opens, you will need to follow platform-specific instructions.
Currently, tracking push open events must be done from the native code due to platform differences that prevent
us from bridging this functionality into the React Native SDK code.

- [Android](https://github.com/klaviyo/klaviyo-android-sdk#Tracking-Open-Events)
- [iOS](https://github.com/klaviyo/klaviyo-swift-sdk#Tracking-Open-Events)

> Note: If you initialize Klaviyo from React Native code, be aware that on both platforms the timing of when
> an `Opened Push` event gets triggered can sometimes occur before your React Native code to initialize our SDK
> can execute. To mitigate this, our SDK holds the request in memory until initialization occurs.

#### Deep Linking

[Deep Links](https://help.klaviyo.com/hc/en-us/articles/14750403974043) allow you to navigate to a particular
Expand Down
1 change: 0 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ android {
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()

}

buildFeatures {
Expand Down
30 changes: 24 additions & 6 deletions example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,25 @@ def enableProguardInReleaseBuilds = false
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc:+'
def localProperties = new Properties()
if (rootProject.file("local.properties").canRead()) {
localProperties.load(new FileInputStream(rootProject.file("local.properties")))
}

android {
def localProperties = new Properties()
if (rootProject.file("local.properties").canRead()) {
localProperties.load(new FileInputStream(rootProject.file("local.properties")))
}
def apiKey = localProperties['publicApiKey'] ?: publicApiKey
//Local properties that will be declared as build config fields:
def apiKey = localProperties['publicApiKey'] ?: publicApiKey
def initializeKlaviyoFromNative = localProperties['initializeKlaviyoFromNative'] ?: initializeKlaviyoFromNative
def useNativeFirebase = localProperties['useNativeFirebase'] ?: useNativeFirebase

android {
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion

namespace "com.klaviyoreactnativesdkexample"
defaultConfig {
// Optional Android Installation Step: Set the applicationId if you want to run this sample app with
// firebase enabled. It must match an applicationId in your google-services.json file
applicationId "com.klaviyoreactnativesdkexample"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
Expand All @@ -100,6 +105,8 @@ android {
debug {
signingConfig signingConfigs.debug
buildConfigField "String", "PUBLIC_API_KEY", "\"${apiKey}\""
buildConfigField "Boolean", "INITIALIZE_KLAVIYO_FROM_NATIVE", "${initializeKlaviyoFromNative}"
buildConfigField "Boolean", "USE_NATIVE_FIREBASE", "${useNativeFirebase}"
}
release {
// Caution! In production, you need to generate your own keystore file.
Expand All @@ -108,6 +115,8 @@ android {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
buildConfigField "String", "PUBLIC_API_KEY", "\"${apiKey}\""
buildConfigField "Boolean", "INITIALIZE_KLAVIYO_FROM_NATIVE", "${initializeKlaviyoFromNative}"
buildConfigField "Boolean", "USE_NATIVE_FIREBASE", "${useNativeFirebase}"
}
}
}
Expand All @@ -117,6 +126,9 @@ dependencies {
implementation("com.facebook.react:react-android")
implementation("com.facebook.react:flipper-integration")

// Android Installation Step 1b - add firebase dependency if using push
implementation 'com.google.firebase:firebase-messaging-ktx:24.0.0'

if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
Expand All @@ -125,3 +137,9 @@ dependencies {
}

apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)

if (useNativeFirebase.toBoolean()) {
// Note: this auto-initializes firebase from your google-services.json file.
// You'll need to set applicationId to match your firebase project, see above
apply plugin: "com.google.gms.google-services"
}
41 changes: 41 additions & 0 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- Android Installation Step 2: Configure manifest.xml ... -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
Expand All @@ -20,6 +21,46 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<!-- Android Installation Step 2a: Configure deep links, if your push notifications will contain them -->
<intent-filter android:label="deep_link_filter">
<action android:name="android.intent.action.VIEW"/>

<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>

<!-- Accepts URIs that begin with "klaviyosample://” -->
<data android:scheme="klaviyosample"/>
</intent-filter>
</activity>

<!-- Android Installation Step 2b: Register KlaviyoPushService to receive MESSAGING_EVENT intents. -->
<service
android:name="com.klaviyo.pushFcm.KlaviyoPushService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>

<!-- Optional Android Installation Step 2c: Specify an icon for Klaviyo notifications -->
<!-- Absent this key, Klaviyo SDK will look for com.google.firebase.messaging.default_notification_icon -->
<!-- and absent that, use the default launcher icon for your app -->
<meta-data
android:name="com.klaviyo.push.default_notification_icon"
android:resource="@drawable/ic_notification" />

<!-- Optional Android Installation Step 2d: Specify a notification color for Klaviyo notifications -->
<!-- Absent this key, Klaviyo SDK will look for com.google.firebase.messaging.default_notification_color -->
<!-- and absent that, omit specifying a color -->
<meta-data android:name="com.klaviyo.push.default_notification_color"
android:resource="@color/notification" />

<!-- Klaviyo Android SDK Debugging Tip: Enable verbose logging from the SDK -->
<!-- You should exclude this from a production build -->
<meta-data
android:name="com.klaviyo.core.log_level"
android:value="1" />

</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
package com.klaviyoreactnativesdkexample

import android.Manifest
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationManagerCompat
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import com.google.firebase.messaging.FirebaseMessaging
import com.klaviyo.analytics.Klaviyo

class MainActivity : ReactActivity() {
/**
Expand All @@ -17,4 +33,139 @@ class MainActivity : ReactActivity() {
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate = DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)

/**
* Launches a permission request, and receives the result in the callback below
*/
private var requestPermissionLauncher: ActivityResultLauncher<String> =
registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) { isGranted: Boolean ->
// This is called with the result of the permission request
val verb = if (isGranted) "granted" else "denied"
Log.d("KlaviyoSampleApp", "Notification permission $verb")

// Android Installation Step 4c: After permission is granted, call setPushToken to update permission state
if (isGranted) {
if (BuildConfig.USE_NATIVE_FIREBASE) {
FirebaseMessaging.getInstance().token.addOnSuccessListener {
Log.d("KlaviyoSampleApp", "Push token set: $it")
Klaviyo.setPushToken(it)
Toast.makeText(
this,
"Permission granted! Push token set.",
Toast.LENGTH_SHORT,
).show()
}
} else {
Toast.makeText(
this,
"Permission granted! Push token not set, because Firebase is not initialized natively.",
Toast.LENGTH_SHORT,
).show()
}
} else {
Toast.makeText(
this,
"Permission denied",
Toast.LENGTH_SHORT,
).show()
}
}

override fun onCreate(savedInstanceState: Bundle?) {
Log.v("KlaviyoSampleApp", "MainActivity.onCreate()")
super.onCreate(savedInstanceState)

// Android Installation Step 4b: Request notification permission from the user, if handling push tokens natively
if (BuildConfig.INITIALIZE_KLAVIYO_FROM_NATIVE) {
// Note: it is not usually advised to prompt for permissions immediately upon app launch. This is just a sample.
when {
NotificationManagerCompat.from(this).areNotificationsEnabled() -> {
// We have already notification permission
Log.v("KlaviyoSampleApp", "Notification permission is granted")
}

ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.POST_NOTIFICATIONS,
) -> {
// Reachable on API level >= 33
// If a permission prompt was previously denied, display an educational UI and request permission again
Log.v("KlaviyoSampleApp", "Requesting notification permission with rationale")
requestPermissionWithRationale()
}

Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
// Reachable on API Level >= 33
// We can request the permission
Log.v("KlaviyoSampleApp", "Requesting notification permission")
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}

else -> {
// Reachable on API Level < 33
// DENIED - Notifications were turned off by the user in system settings
Log.v("KlaviyoSampleApp", "Notification permission is denied and won't be requested")
alertPermissionDenied()
}
}
}

// Android Installation Step 5a: Depending on the state of your application when the notification is tapped,
// the intent have started this activity, or it might be received via onNewIntent if the app was already running.
// We recommend passing all intents through Klaviyo.handlePush to make sure you don't miss a use case.
onNewIntent(intent)
}

override fun onNewIntent(intent: Intent?) {
Log.v("KlaviyoSampleApp", "MainActivity.onNewIntent()")
Log.v("KlaviyoSampleApp", "Launch Intent: " + intent.toString())
super.onNewIntent(intent)

// Android Installation Step 5: Call handlePush when a push notification is tapped
// Note: due to platform differences, this step must be implemented in native code.
// Tapping on a notification broadcasts an intent to your app. This method detects if the
// intent originated from a Klaviyo push notification and registers a special Opened Push event
Klaviyo.handlePush(intent)

// Android Installation Step 6: Deep linking from native layer (uncommon)
// Read deep link data from intent, open the appropriate page
val action: String? = intent?.action // e.g. ACTION_VIEW
val deepLink: Uri? = intent?.data // e.g. klaviyoreactnativesdkexample://link
}

@SuppressLint("InlinedApi") // It is safe to use Manifest.permission.POST_NOTIFICATIONS, ActivityCompat handles API level differences
private fun requestPermissionWithRationale() =
AlertDialog.Builder(this)
.setTitle("Notifications Permission")
.setMessage("Permission must be granted in order to receive push notifications in the system tray.")
.setCancelable(true)
.setPositiveButton("Grant") { _, _ ->
// You can directly ask for the permission.
// The registered ActivityResultCallback gets the result of this request.
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
.setNegativeButton("Cancel") { _, _ -> }
.show()

private fun alertPermissionDenied(): AlertDialog =
AlertDialog.Builder(this)
.setTitle("Notifications Disabled")
.setMessage("Permission is denied and can only be changed from notification settings.")
.setCancelable(true)
.setPositiveButton("Settings...") { _, _ -> openSettings() }
.setNegativeButton("Cancel") { _, _ -> }
.show()

private fun openSettings() {
val intent =
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", packageName, null),
)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK

startActivity(intent)
}
}
Loading

0 comments on commit 3d10d55

Please sign in to comment.