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
80 changes: 80 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,55 @@ 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 synthetic fun <init> (Ljava/util/List;Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
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 synthetic fun <init> (Lcom/onesignal/ErrorCode;Ljava/lang/Integer;Ljava/lang/String;Lcom/onesignal/ErrorSource;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getBackendCode ()Ljava/lang/Integer;
public final fun getCode ()Lcom/onesignal/ErrorCode;
public final fun getMessage ()Ljava/lang/String;
public final fun getSource ()Lcom/onesignal/ErrorSource;
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 synthetic fun <init> (Lcom/onesignal/OneSignalResultData;Lcom/onesignal/OneSignalError;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
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,224 @@
package com.onesignal

import com.onesignal.common.toList
import com.onesignal.common.toMap
import org.json.JSONArray
import org.json.JSONObject
import java.util.Collections

/** 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": "..." } ] }
* ```
*
* The constructor is private on purpose. An `internal` constructor still emits as JVM-public, so
* Java outside this module could build an error with no reasons and leave [first] throwing; private
* closes that hole. Callers construct through [of] or [fromWire].
*/
class OneSignalError private 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?,
) {
/**
* Why the call failed. Never empty.
*
* Copied so a caller holding the original list cannot empty it afterwards, and unmodifiable so
* the copy itself cannot be emptied either. Java sees a plain `List` and `clear()` is one
* keystroke away from `get()`; both would leave [first] throwing.
*/
val error: List<Detail> = Collections.unmodifiableList(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 private 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

the thought was, there are existing error status that we want to return, mostly on the client side. I was trying to figure out a way where we can differentiate between client (we know of) and server codes that we are unaware of. And this was a better representation of it that seems to be scalable.

/**
* 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?,
/** A human-readable description intended for logs and diagnostics, not for end users. */
val message: String?,
/**
* Who the failure came from.
*
* Carried rather than derived from [code] on demand, because a code this SDK does not
* recognize degrades to [ErrorCode.UNKNOWN] and re-deriving from that would report a
* backend failure as a client one. Defaults to the source [code] implies, which is right
* for everything the SDK raises locally.
*/
val source: ErrorSource,
) {
/** 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 source.name,
KEY_BACKEND_CODE to backendCode,
KEY_MESSAGE to message,
)

override fun toString(): String = "Detail(code=$code, source=$source, 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.
*
* Reads a raw map because the bridges do not all hand over `Map<String, Any?>`
* specifically, and because an unchecked cast that failed would be indistinguishable
* from a reason that was never there.
*
* 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. [message], [backendCode] and [source] are preserved either way, which
* is what keeps a degraded reason diagnosable.
*/
fun fromMap(map: Map<*, *>): Detail {
val code = codeOf(map[KEY_CODE] as? String)
return Detail(
code = code,
backendCode = (map[KEY_BACKEND_CODE] as? Number)?.toInt(),
message = map[KEY_MESSAGE] as? String,
source = sourceOf(map[KEY_SOURCE] as? String) ?: code.source,
)
}

fun of(
code: ErrorCode,
backendCode: Int? = null,
message: String? = null,
source: ErrorSource = code.source,
): Detail = Detail(code, backendCode, message, source)

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

private fun sourceOf(name: String?): ErrorSource? = ErrorSource.entries.firstOrNull { it.name == name }
}
}

/** 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.of(code, backendCode, message)), cause)

/** Builds a multi-reason error. [reasons] must not be empty. */
fun of(
reasons: List<Detail>,
cause: Throwable? = null,
): OneSignalError = OneSignalError(reasons, cause)

/**
* Rebuilds an error from its wire shape.
*
* Takes the raw value rather than a typed list because the bridges do not all hand over a
* [List] — org.json's array is not one. Anything a producer put under `error` is a failure
* being reported, so an unreadable shape becomes a reason carrying its own text rather than
* being dropped, which would silently turn the failure into a success.
*
* A payload carrying no recognizable reason still yields a usable error rather than an
* empty list, so [first] is always safe.
*/
fun fromWire(raw: Any?): OneSignalError {
val reasons =
when (raw) {
is List<*> -> raw.map { reasonOf(it) }
// org.json is what a bridge naturally parses with, and JSONArray is not a
// java.util.List. Convert rather than treating the whole array as one reason.
is JSONArray -> raw.toList().orEmpty().map { reasonOf(it) }
else -> listOf(reasonOf(raw))
}
return OneSignalError(reasons.ifEmpty { listOf(Detail.of(ErrorCode.UNKNOWN)) }, cause = null)
}

private fun reasonOf(raw: Any?): Detail =
when (raw) {
is Map<*, *> -> Detail.fromMap(raw)
is JSONObject -> Detail.fromMap(raw.toMap())
else -> Detail.of(ErrorCode.UNKNOWN, message = raw?.toString())
}
}
}
Loading