Skip to content

Commit

Permalink
Merge branch 'main' into paypal-app-switch-feature
Browse files Browse the repository at this point in the history
  • Loading branch information
richherrera committed Sep 18, 2024
2 parents 6c9af2d + a14173b commit 19ea4a3
Show file tree
Hide file tree
Showing 196 changed files with 1,774 additions and 1,802 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ package com.braintreepayments.api.americanexpress

import android.content.Context
import android.net.Uri
import androidx.annotation.RestrictTo
import com.braintreepayments.api.core.ApiClient.Companion.versionedPath
import com.braintreepayments.api.core.BraintreeClient
import org.json.JSONException

/**
* Used to integrate with Braintree's American Express API
*/
class AmericanExpressClient @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) constructor(
class AmericanExpressClient internal constructor(
private val braintreeClient: BraintreeClient
) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ sealed class AmericanExpressResult {
/**
* The [rewardsBalance] was successfully fetched
*/
class Success(val rewardsBalance: AmericanExpressRewardsBalance) : AmericanExpressResult()
class Success internal constructor(
val rewardsBalance: AmericanExpressRewardsBalance
) : AmericanExpressResult()

/**
* There was an [error] fetching rewards balance
*/
class Failure(val error: Exception) : AmericanExpressResult()
class Failure internal constructor(val error: Exception) : AmericanExpressResult()
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.braintreepayments.api.americanexpress

import android.os.Parcelable
import androidx.annotation.RestrictTo
import com.braintreepayments.api.sharedutils.Json
import kotlinx.parcelize.Parcelize
import org.json.JSONException
Expand All @@ -20,7 +19,7 @@ import org.json.JSONObject
* @property rewardsUnit - The rewards unit associated with the rewards balance
*/
@Parcelize
data class AmericanExpressRewardsBalance(
data class AmericanExpressRewardsBalance @JvmOverloads constructor(
var errorCode: String? = null,
var errorMessage: String? = null,
var conversionRate: String? = null,
Expand Down Expand Up @@ -49,10 +48,8 @@ data class AmericanExpressRewardsBalance(
* @return The [AmericanExpressRewardsBalance] with rewards balance data.
* @throws JSONException when parsing fails.
*/
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Throws(JSONException::class)
fun fromJson(jsonString: String): AmericanExpressRewardsBalance {
internal fun fromJson(jsonString: String): AmericanExpressRewardsBalance {
val json = JSONObject(jsonString)

val rewardsBalance = AmericanExpressRewardsBalance()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "312082ac4a200e3aa820a34e9dbf6e7a",
"entities": [
{
"tableName": "analytics_event_blob",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `json_string` TEXT NOT NULL, `sessionId` TEXT NOT NULL DEFAULT '')",
"fields": [
{
"fieldPath": "id",
"columnName": "_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "jsonString",
"columnName": "json_string",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sessionId",
"columnName": "sessionId",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"_id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '312082ac4a200e3aa820a34e9dbf6e7a')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class AnalyticsClientTest {
val event = AnalyticsEvent("event.started")
val sut = AnalyticsClient(context)
val workSpecId =
sut.sendEvent(configuration, event, "sessionId", IntegrationType.CUSTOM, authorization)
sut.sendEvent(configuration, event, IntegrationType.CUSTOM, authorization)

val workInfoBeforeDelay =
WorkManager.getInstance(context).getWorkInfoById(workSpecId).get()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,50 +19,54 @@ internal class AnalyticsClient(
private val httpClient: BraintreeHttpClient = BraintreeHttpClient(),
private val analyticsDatabase: AnalyticsDatabase = AnalyticsDatabase.getInstance(context.applicationContext),
private val workManager: WorkManager = WorkManager.getInstance(context.applicationContext),
private val deviceInspector: DeviceInspector = DeviceInspector()
private val deviceInspector: DeviceInspector = DeviceInspector(),
private val analyticsParamRepository: AnalyticsParamRepository = AnalyticsParamRepository.instance
) {
private val applicationContext = context.applicationContext

fun sendEvent(
configuration: Configuration,
event: AnalyticsEvent,
sessionId: String?,
integration: IntegrationType?,
authorization: Authorization
): UUID {
scheduleAnalyticsWriteInBackground(event, authorization)
return scheduleAnalyticsUploadInBackground(
configuration,
authorization,
sessionId,
integration
)
}

private fun scheduleAnalyticsWriteInBackground(
event: AnalyticsEvent, authorization: Authorization
event: AnalyticsEvent,
authorization: Authorization
) {
val json = mapAnalyticsEventToFPTIEventJSON(event)
val inputData = Data.Builder()
.putString(WORK_INPUT_KEY_AUTHORIZATION, authorization.toString())
.putString(WORK_INPUT_KEY_ANALYTICS_JSON, json)
.putString(WORK_INPUT_KEY_SESSION_ID, analyticsParamRepository.sessionId)
.build()

val analyticsWorkRequest =
OneTimeWorkRequest.Builder(AnalyticsWriteToDbWorker::class.java)
.setInputData(inputData)
.build()
val analyticsWorkRequest = OneTimeWorkRequest.Builder(AnalyticsWriteToDbWorker::class.java)
.setInputData(inputData)
.build()
workManager.enqueueUniqueWork(
WORK_NAME_ANALYTICS_WRITE, ExistingWorkPolicy.APPEND_OR_REPLACE, analyticsWorkRequest
)
}

fun performAnalyticsWrite(inputData: Data): ListenableWorker.Result {
val analyticsJSON = inputData.getString(WORK_INPUT_KEY_ANALYTICS_JSON)
return if (analyticsJSON == null) {
val sessionId = inputData.getString(WORK_INPUT_KEY_SESSION_ID)
return if (analyticsJSON == null || sessionId == null) {
ListenableWorker.Result.failure()
} else {
val eventBlob = AnalyticsEventBlob(analyticsJSON)
val eventBlob = AnalyticsEventBlob(
jsonString = analyticsJSON,
sessionId = sessionId
)
val analyticsBlobDao = analyticsDatabase.analyticsEventBlobDao()
analyticsBlobDao.insertEventBlob(eventBlob)
ListenableWorker.Result.success()
Expand All @@ -72,9 +76,9 @@ internal class AnalyticsClient(
private fun scheduleAnalyticsUploadInBackground(
configuration: Configuration,
authorization: Authorization,
sessionId: String?,
integration: IntegrationType?
): UUID {
val sessionId = analyticsParamRepository.sessionId
val inputData = Data.Builder()
.putString(WORK_INPUT_KEY_AUTHORIZATION, authorization.toString())
.putString(WORK_INPUT_KEY_CONFIGURATION, configuration.toJson())
Expand All @@ -87,7 +91,9 @@ internal class AnalyticsClient(
.setInputData(inputData)
.build()
workManager.enqueueUniqueWork(
WORK_NAME_ANALYTICS_UPLOAD, ExistingWorkPolicy.KEEP, analyticsWorkRequest
WORK_NAME_ANALYTICS_UPLOAD + sessionId,
ExistingWorkPolicy.KEEP,
analyticsWorkRequest
)
return analyticsWorkRequest.id
}
Expand All @@ -97,48 +103,49 @@ internal class AnalyticsClient(
val authorization = getAuthorizationFromData(inputData)
val sessionId = inputData.getString(WORK_INPUT_KEY_SESSION_ID)
val integration = inputData.getString(WORK_INPUT_KEY_INTEGRATION)
val isMissingInputData =
listOf(configuration, authorization, sessionId, integration).contains(null)
return if (isMissingInputData) {
ListenableWorker.Result.failure()
} else {
try {
val analyticsEventBlobDao = analyticsDatabase.analyticsEventBlobDao()
val eventBlobs = analyticsEventBlobDao.getAllEventBlobs()
if (eventBlobs.isNotEmpty()) {
val metadata = deviceInspector.getDeviceMetadata(
applicationContext,
configuration,
sessionId,
IntegrationType.fromString(integration)
)
val analyticsRequest = createFPTIPayload(authorization, eventBlobs, metadata)
httpClient.post(
FPTI_ANALYTICS_URL,
analyticsRequest.toString(),
configuration,
authorization
)
analyticsEventBlobDao.deleteEventBlobs(eventBlobs)
}
ListenableWorker.Result.success()
} catch (e: Exception) {
return when (null) {
configuration, authorization, sessionId, integration -> {
ListenableWorker.Result.failure()
}

else -> {
try {
val analyticsEventBlobDao = analyticsDatabase.analyticsEventBlobDao()
val eventBlobs = analyticsEventBlobDao.getBlobsBySessionId(sessionId)
if (eventBlobs.isNotEmpty()) {
val metadata = deviceInspector.getDeviceMetadata(
applicationContext,
configuration,
sessionId,
IntegrationType.fromString(integration)
)
val analyticsRequest =
createFPTIPayload(authorization, eventBlobs, metadata)
httpClient.post(
FPTI_ANALYTICS_URL,
analyticsRequest.toString(),
configuration,
authorization
)
analyticsEventBlobDao.deleteEventBlobs(eventBlobs)
}
ListenableWorker.Result.success()
} catch (e: Exception) {
ListenableWorker.Result.failure()
}
}
}
}

fun reportCrash(
context: Context?,
configuration: Configuration?,
sessionId: String?,
integration: IntegrationType?,
authorization: Authorization?
) {
reportCrash(
context,
configuration,
sessionId,
integration,
System.currentTimeMillis(),
authorization
Expand All @@ -149,19 +156,27 @@ internal class AnalyticsClient(
fun reportCrash(
context: Context?,
configuration: Configuration?,
sessionId: String?,
integration: IntegrationType?,
timestamp: Long,
authorization: Authorization?
) {
if (authorization == null) {
return
}
val metadata =
deviceInspector.getDeviceMetadata(context, configuration, sessionId, integration)
val metadata = deviceInspector.getDeviceMetadata(
context = context,
configuration = configuration,
sessionId = analyticsParamRepository.sessionId,
integration = integration
)
val event = AnalyticsEvent(name = "crash", timestamp = timestamp)
val eventJSON = mapAnalyticsEventToFPTIEventJSON(event)
val eventBlobs = listOf(AnalyticsEventBlob(eventJSON))
val eventBlobs = listOf(
AnalyticsEventBlob(
jsonString = eventJSON,
sessionId = ""
)
)
try {
val analyticsRequest = createFPTIPayload(authorization, eventBlobs, metadata)
httpClient.post(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ import androidx.room.migration.AutoMigrationSpec

// Ref: https://developer.android.com/training/data-storage/room/migrating-db-versions
@Database(
version = 7,
version = 8,
entities = [AnalyticsEventBlob::class],
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
AutoMigration(from = 3, to = 4),
AutoMigration(from = 4, to = 5),
AutoMigration(from = 5, to = 6),
AutoMigration(from = 6, to = 7, spec = AnalyticsDatabase.DeleteAnalyticsEventTableAutoMigration::class)
AutoMigration(from = 6, to = 7, spec = AnalyticsDatabase.DeleteAnalyticsEventTableAutoMigration::class),
AutoMigration(from = 7, to = 8)
]
)
internal abstract class AnalyticsDatabase : RoomDatabase() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import androidx.room.PrimaryKey
* at the JSON level. JSON encoded events can be sent directly to the analytics server.
*/
@Entity(tableName = "analytics_event_blob")
data class AnalyticsEventBlob(
internal data class AnalyticsEventBlob(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "_id") val id: Long = 0L,
@ColumnInfo(name = "json_string") val jsonString: String,
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "_id") val id: Long = 0L
@ColumnInfo(defaultValue = "") val sessionId: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ internal interface AnalyticsEventBlobDao {
@Insert
fun insertEventBlob(eventBlob: AnalyticsEventBlob)

@Query("SELECT * FROM analytics_event_blob")
fun getAllEventBlobs(): List<AnalyticsEventBlob>
@Query("SELECT * FROM analytics_event_blob WHERE sessionId = :sessionId")
fun getBlobsBySessionId(sessionId: String): List<AnalyticsEventBlob>

@Delete
fun deleteEventBlobs(blobs: List<AnalyticsEventBlob>)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.braintreepayments.api.core

import androidx.annotation.RestrictTo

/**
* This class is responsible for holding parameters that are sent with analytic events.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class AnalyticsParamRepository(
private val uuidHelper: UUIDHelper = UUIDHelper()
) {

private lateinit var _sessionId: String

/**
* Session ID to tie analytics events together which is used for reporting conversion funnels.
*/
val sessionId: String
get() {
if (!this::_sessionId.isInitialized) {
_sessionId = uuidHelper.formattedUUID
}
return _sessionId
}

/**
* Clears the session ID value from the repository
*/
fun resetSessionId() {
_sessionId = uuidHelper.formattedUUID
}

companion object {

/**
* Singleton instance of the AnalyticsParamRepository.
*/
val instance: AnalyticsParamRepository by lazy { AnalyticsParamRepository() }
}
}
Loading

0 comments on commit 19ea4a3

Please sign in to comment.