Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
76 changes: 76 additions & 0 deletions OneSignalSDK/onesignal/core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,26 @@ public final class com/onesignal/ContinueResult {
public final fun isSuccess ()Z
}

public final class com/onesignal/ErrorCode : java/lang/Enum {
public static final field BACKEND_ERROR Lcom/onesignal/ErrorCode;
public static final field INVALID_ARGUMENT Lcom/onesignal/ErrorCode;
public static final field NOT_INITIALIZED Lcom/onesignal/ErrorCode;
public static final field STORAGE_LOCKED Lcom/onesignal/ErrorCode;
public static final field UNKNOWN Lcom/onesignal/ErrorCode;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public final fun getSource ()Lcom/onesignal/ErrorSource;
public static fun valueOf (Ljava/lang/String;)Lcom/onesignal/ErrorCode;
public static fun values ()[Lcom/onesignal/ErrorCode;
}

public final class com/onesignal/ErrorSource : java/lang/Enum {
public static final field BACKEND Lcom/onesignal/ErrorSource;
public static final field CLIENT Lcom/onesignal/ErrorSource;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/onesignal/ErrorSource;
public static fun values ()[Lcom/onesignal/ErrorSource;
}

public abstract interface class com/onesignal/IOneSignal {
public abstract fun addUserJwtInvalidatedListener (Lcom/onesignal/IUserJwtInvalidatedListener;)V
public abstract fun getConsentGiven ()Z
Expand Down Expand Up @@ -64,6 +84,23 @@ public abstract interface class com/onesignal/IUserJwtInvalidatedListener {
public abstract fun onUserJwtInvalidated (Lcom/onesignal/UserJwtInvalidatedEvent;)V
}

public final class com/onesignal/InitData : com/onesignal/OneSignalResultData {
public fun toMap ()Ljava/util/Map;
public fun toString ()Ljava/lang/String;
}

public final class com/onesignal/LoginData : com/onesignal/OneSignalResultData {
public final fun getExternalId ()Ljava/lang/String;
public final fun getOnesignalId ()Ljava/lang/String;
public fun toMap ()Ljava/util/Map;
public fun toString ()Ljava/lang/String;
}

public final class com/onesignal/LogoutData : com/onesignal/OneSignalResultData {
public fun toMap ()Ljava/util/Map;
public fun toString ()Ljava/lang/String;
}

public final class com/onesignal/OneSignal {
public static final field INSTANCE Lcom/onesignal/OneSignal;
public static final fun addUserJwtInvalidatedListener (Lcom/onesignal/IUserJwtInvalidatedListener;)V
Expand Down Expand Up @@ -109,12 +146,51 @@ public final class com/onesignal/OneSignal {
public static final fun updateUserJwtSuspend (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class com/onesignal/OneSignalError {
public final fun getCause ()Ljava/lang/Throwable;
public final fun getError ()Ljava/util/List;
public final fun getFirst ()Lcom/onesignal/OneSignalError$Detail;
public final fun toList ()Ljava/util/List;
public fun toString ()Ljava/lang/String;
}

public final class com/onesignal/OneSignalError$Detail {
public final fun getBackendCode ()Ljava/lang/Integer;
public final fun getCode ()Lcom/onesignal/ErrorCode;
public final fun getMessage ()Ljava/lang/String;
public final fun toMap ()Ljava/util/Map;
public fun toString ()Ljava/lang/String;
}

public final class com/onesignal/OneSignalException : java/lang/Exception {
public final fun getError ()Lcom/onesignal/OneSignalError;
}

public final class com/onesignal/OneSignalResult {
public final fun getData ()Lcom/onesignal/OneSignalResultData;
public final fun getError ()Lcom/onesignal/OneSignalError;
public final fun getOrNull ()Lcom/onesignal/OneSignalResultData;
public final fun getOrThrow ()Lcom/onesignal/OneSignalResultData;
public final fun isSuccess ()Z
public final fun toMap ()Ljava/util/Map;
public fun toString ()Ljava/lang/String;
}

public abstract interface class com/onesignal/OneSignalResultData {
public abstract fun toMap ()Ljava/util/Map;
}

public final class com/onesignal/SyncJobService : android/app/job/JobService {
public fun <init> ()V
public fun onStartJob (Landroid/app/job/JobParameters;)Z
public fun onStopJob (Landroid/app/job/JobParameters;)Z
}

public final class com/onesignal/UpdateUserJwtData : com/onesignal/OneSignalResultData {
public fun toMap ()Ljava/util/Map;
public fun toString ()Ljava/lang/String;
}

public final class com/onesignal/UserJwtInvalidatedEvent {
public fun <init> (Ljava/lang/String;)V
public final fun getExternalId ()Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package com.onesignal

/** Whether the SDK produced a failure locally or OneSignal's backend returned it. */
enum class ErrorSource {
CLIENT,
BACKEND,
}

/**
* The catalog of failure codes shared by every OneSignal SDK.
*
* An enum rather than a sealed hierarchy so that Java callers get a native `switch` and the
* wrapper bridges get a trivial name-to-string marshal. The backend half of the catalog is
* deliberately *not* modelled here — see [BACKEND_ERROR].
*/
enum class ErrorCode(val source: ErrorSource) {
/** [IOneSignal.initWithContextSuspend] has not been called. */
NOT_INITIALIZED(ErrorSource.CLIENT),

/**
* Device storage was locked, so the SDK could not read or write its own preferences.
* Transient: the same call generally succeeds once the device is unlocked.
*/
STORAGE_LOCKED(ErrorSource.CLIENT),

/** A caller-supplied argument failed validation before any request was made. */
INVALID_ARGUMENT(ErrorSource.CLIENT),

/** OneSignal rejected the request. The catalog code is on [OneSignalError.Detail.backendCode]. */
BACKEND_ERROR(ErrorSource.BACKEND),

/** No more specific code applies. Callers should surface [OneSignalError.Detail.message]. */
UNKNOWN(ErrorSource.CLIENT),
}

/**
* Describes why a OneSignal call failed.
*
* One request can fail for several reasons at once, so [error] is a list of [Detail]. Everything
* the SDK raises locally has exactly one reason, which [first] reads without the indexing
* ceremony.
*
* On the wire this is the list itself, sitting under the envelope's `error` key:
*
* ```json
* { "success": false, "data": null,
* "error": [ { "code": "STORAGE_LOCKED", "source": "CLIENT", "backendCode": null, "message": "..." } ] }
* ```
*/
class OneSignalError internal constructor(
error: List<Detail>,
/**
* The throwable behind the failure, when there was one.
*
* Deliberately absent from [toList]: a stack trace cannot cross the wrapper bridges, and the
* wire schema has to stay identical across every SDK. This exists so that native Kotlin and
* Java callers do not lose the stack when the suspend APIs report a failure instead of
* throwing it.
*/
val cause: Throwable? = null,
) {
/**
* Why the call failed. Never empty.
*
* Copied rather than aliased so that a caller holding the original list cannot empty it
* afterwards and leave [first] throwing.
*/
val error: List<Detail> = error.toList()

init {
// [first] is documented as always safe to read, and the wire projection of an empty error
// would claim failure while explaining nothing. Both factories guard this; the check is
// here so a future caller of the constructor cannot quietly break the invariant.
require(this.error.isNotEmpty()) { "OneSignalError requires at least one Detail." }
}

/**
* A single reason a call failed.
*
* Nested rather than top-level so the name cannot collide with `kotlin.Error`, which is
* auto-imported everywhere, or shadow `java.lang.Error` in a Java file that imports it.
*/
class Detail internal constructor(
/** A stable code, safe to branch on. Never localized. */
val code: ErrorCode,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i thought were avoiding error codes

/**
* The backend's catalog code, present only when [code] is [ErrorCode.BACKEND_ERROR].
*
* Left as a raw number on purpose: the backend adds codes on its own schedule, and an SDK
* release must not be the thing that unblocks recognizing one.
*/
val backendCode: Int? = null,
/** A human-readable description intended for logs and diagnostics, not for end users. */
val message: String? = null,
) {
/** Projects this reason onto the cross-SDK wire shape consumed by the wrapper bridges. */
fun toMap(): Map<String, Any?> =
mapOf(
KEY_CODE to code.name,
KEY_SOURCE to code.source.name,
KEY_BACKEND_CODE to backendCode,
KEY_MESSAGE to message,
)

override fun toString(): String = "Detail(code=$code, backendCode=$backendCode, message=$message)"

internal companion object {
// Private because `const val` in an internal companion still compiles to a public
// static field, which would leak the wire keys into the customer-facing API surface.
private const val KEY_CODE = "code"
private const val KEY_SOURCE = "source"
private const val KEY_BACKEND_CODE = "backendCode"
private const val KEY_MESSAGE = "message"

/**
* Rebuilds a reason from its wire shape.
*
* An unrecognized code degrades to [ErrorCode.UNKNOWN] rather than throwing, so a
* wrapper built against an older SDK survives a newer producer emitting a code it has
* never heard of. The original text is preserved on [message] either way.
*/
fun fromMap(map: Map<String, Any?>): Detail =
Detail(
code = codeOf(map[KEY_CODE] as? String),
backendCode = (map[KEY_BACKEND_CODE] as? Number)?.toInt(),
message = map[KEY_MESSAGE] as? String,
)

private fun codeOf(name: String?): ErrorCode = ErrorCode.entries.firstOrNull { it.name == name } ?: ErrorCode.UNKNOWN
}
}

/** The first reason, which is the only one for every failure the SDK raises locally. */
val first: Detail
get() = error.first()

/** Projects this error onto the cross-SDK wire shape consumed by the wrapper bridges. */
fun toList(): List<Map<String, Any?>> = error.map { it.toMap() }

override fun toString(): String = "OneSignalError(error=$error)"

internal companion object {
/** Builds a single-reason error, which is the shape of everything the SDK raises locally. */
fun of(
code: ErrorCode,
message: String? = null,
backendCode: Int? = null,
cause: Throwable? = null,
): OneSignalError = OneSignalError(listOf(Detail(code, backendCode, message)), cause)

/**
* Rebuilds an error from its wire shape. A payload carrying no recognizable reason still
* yields a usable error rather than an empty list, so [first] is always safe.
*/
fun fromList(reasons: List<Map<String, Any?>>): OneSignalError =
OneSignalError(
reasons.map { Detail.fromMap(it) }.takeIf { it.isNotEmpty() } ?: listOf(Detail(ErrorCode.UNKNOWN)),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.onesignal

/**
* The outcome of an asynchronous OneSignal call: either a [data] payload or an [error], never both
* and never neither.
*
* Every SDK returns the same envelope, so a wrapper can handle results uniformly regardless of the
* platform underneath it:
*
* ```json
* { "success": true, "data": { }, "error": null }
* { "success": false, "data": null, "error": [ { "code": "STORAGE_LOCKED", ... } ] }
* ```
*
* The presence of [error] is what defines the outcome; [isSuccess] and the wire-level `success`
* flag are both derived from it, so the two can never disagree.
*
* From Kotlin:
* ```kotlin
* val result = OneSignal.login("user-123")
* if (result.isSuccess) println(result.data?.onesignalId) else println(result.error?.first?.code)
* ```
*
* From Java the generated accessors read naturally:
* ```java
* if (result.isSuccess()) { result.getData(); } else { result.getError(); }
* ```
*/
class OneSignalResult<T : OneSignalResultData> internal constructor(
/** The payload on success, `null` on failure. */
val data: T?,
/** The failure detail on failure, `null` on success. */
val error: OneSignalError?,
) {
/** `true` when the call completed successfully. Equivalent to `error == null`. */
val isSuccess: Boolean
get() = error == null

/** Kotlin-idiomatic alias for [data]. */
fun getOrNull(): T? = data

/**
* Returns the payload, or throws [OneSignalException] when the call failed. Use this only where
* a failure genuinely cannot be handled locally.
*/
fun getOrThrow(): T = data ?: throw OneSignalException(error ?: unexpectedMissingError())

/** Projects the envelope onto the cross-SDK wire shape consumed by the wrapper bridges. */
fun toMap(): Map<String, Any?> =
mapOf(
KEY_SUCCESS to isSuccess,
KEY_DATA to data?.toMap(),
KEY_ERROR to error?.toList(),
)

override fun toString(): String = if (isSuccess) "OneSignalResult(success, data=$data)" else "OneSignalResult(failure, error=$error)"

private fun unexpectedMissingError() = OneSignalError.of(ErrorCode.UNKNOWN, "Result carried neither data nor error.")

internal companion object {
// Private because `const val` in an internal companion still compiles to a public static
// field, which would leak the wire keys into the customer-facing API surface.
private const val KEY_SUCCESS = "success"
private const val KEY_DATA = "data"
private const val KEY_ERROR = "error"

fun <T : OneSignalResultData> success(data: T): OneSignalResult<T> = OneSignalResult(data, null)

fun <T : OneSignalResultData> failure(error: OneSignalError): OneSignalResult<T> = OneSignalResult(null, error)

fun <T : OneSignalResultData> failure(
code: ErrorCode,
message: String? = null,
backendCode: Int? = null,
cause: Throwable? = null,
): OneSignalResult<T> = failure(OneSignalError.of(code, message, backendCode, cause))

/**
* Rebuilds an envelope from its wire shape, delegating payload parsing to [dataParser].
*
* The incoming `success` flag is deliberately ignored: [error] is the single source of
* truth, which keeps a malformed producer from yielding a result that claims success while
* carrying an error. Unrecognized keys are ignored so a newer producer can add fields
* without breaking an older consumer.
*/
@Suppress("UNCHECKED_CAST")
fun <T : OneSignalResultData> fromMap(
map: Map<String, Any?>,
dataParser: (Map<String, Any?>) -> T,
): OneSignalResult<T> {
val reasons = map[KEY_ERROR] as? List<Map<String, Any?>>
if (reasons != null) {
return failure(OneSignalError.fromList(reasons))
}

val dataMap = map[KEY_DATA] as? Map<String, Any?> ?: emptyMap()
return success(dataParser(dataMap))
}
}
}

/** Thrown by [OneSignalResult.getOrThrow] when the underlying call failed. */
class OneSignalException internal constructor(
/** The failure detail that caused this exception. */
val error: OneSignalError,
) : Exception(describe(error), error.cause)

// A Detail carries no message when the code says everything, so appending a bare "null" to the
// exception text would only add noise to the stack trace.
private fun describe(error: OneSignalError): String =
error.error.joinToString("; ") { detail ->
if (detail.message == null) detail.code.name else "${detail.code}: ${detail.message}"
}
Loading