hiqmobiad_sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
- package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
- package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/9.2.0/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build.gradle +58 -0
- package/android/proguard-rules.pro +20 -0
- package/android/src/main/AndroidManifest.xml +18 -0
- package/android/src/main/java/com/ownad/adsdk/AdRepository.kt +201 -0
- package/android/src/main/java/com/ownad/adsdk/AdSdkModule.kt +249 -0
- package/android/src/main/java/com/ownad/adsdk/AdSdkPackage.kt +17 -0
- package/android/src/main/java/com/ownad/adsdk/VideoAdActivity.kt +293 -0
- package/android/src/main/res/drawable/ownad_bottom_card_bg.xml +9 -0
- package/android/src/main/res/drawable/ownad_circle_button_bg.xml +9 -0
- package/android/src/main/res/drawable/ownad_install_button_bg.xml +9 -0
- package/android/src/main/res/layout/activity_video_ad.xml +116 -0
- package/package.json +38 -0
- package/src/AdSdk.ts +16 -0
- package/src/RewardedAd.ts +132 -0
- package/src/index.ts +4 -0
- package/src/types.ts +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# OwnAd SDK
|
|
2
|
+
|
|
3
|
+
React Native SDK for a rewarded video ad flow. The native Android screen now uses
|
|
4
|
+
your live creative payload and renders a full-screen background video with:
|
|
5
|
+
|
|
6
|
+
- top-left back affordance
|
|
7
|
+
- mute toggle
|
|
8
|
+
- bottom app card with logo, app name, and install button
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @priyahiq/own-ad-sdk
|
|
14
|
+
cd android && ./gradlew
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Android Autolinking
|
|
18
|
+
|
|
19
|
+
React Native 0.60+ autolinks the package. If you link manually, add this to
|
|
20
|
+
`MainApplication`:
|
|
21
|
+
|
|
22
|
+
```kotlin
|
|
23
|
+
import com.ownad.adsdk.AdSdkPackage
|
|
24
|
+
|
|
25
|
+
override fun getPackages(): List<ReactPackage> =
|
|
26
|
+
PackageList(this).packages.apply { add(AdSdkPackage()) }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Using The SDK
|
|
30
|
+
|
|
31
|
+
### 1. Initialize once at app start
|
|
32
|
+
|
|
33
|
+
Point the SDK to your API base URL:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { initialize } from '@priyahiq/own-ad-sdk';
|
|
37
|
+
|
|
38
|
+
await initialize('YOUR_APP_ID', {
|
|
39
|
+
baseUrl: 'https://myownadsprodapi.hiqmobi.in/api',
|
|
40
|
+
debug: __DEV__,
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2. Load and show a rewarded ad
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { RewardedAd } from '@priyahiq/own-ad-sdk';
|
|
48
|
+
|
|
49
|
+
const ad = RewardedAd.createForAdRequest('YOUR_AD_UNIT_ID');
|
|
50
|
+
|
|
51
|
+
ad.addListener({
|
|
52
|
+
onLoaded: () => ad.show(),
|
|
53
|
+
onShown: () => console.log('ad shown'),
|
|
54
|
+
onClicked: () => console.log('install clicked'),
|
|
55
|
+
onFailedToLoad: (e) => console.warn('load failed', e.code, e.message),
|
|
56
|
+
onFailedToShow: (e) => console.warn('show failed', e.code, e.message),
|
|
57
|
+
onCompleted: () => {
|
|
58
|
+
// The video finished. The ad can now be closed safely.
|
|
59
|
+
ad.close();
|
|
60
|
+
},
|
|
61
|
+
onUserEarnedReward: (reward) => {
|
|
62
|
+
console.log(`earned ${reward.amount} ${reward.type}`);
|
|
63
|
+
},
|
|
64
|
+
onDismissed: () => console.log('ad dismissed'),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
ad.load();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Flow
|
|
71
|
+
|
|
72
|
+
1. `load()` requests one creative from `GET /v1/ad`
|
|
73
|
+
2. the native activity receives:
|
|
74
|
+
- `title`
|
|
75
|
+
- `app_name`
|
|
76
|
+
- `logo_url`
|
|
77
|
+
- `background_video_url`
|
|
78
|
+
- `background_image_url`
|
|
79
|
+
- `button_text`
|
|
80
|
+
- `click_url`
|
|
81
|
+
- `min_watch_seconds`
|
|
82
|
+
3. `show()` opens the full-screen player
|
|
83
|
+
4. the install button opens `click_url`
|
|
84
|
+
5. when playback ends, `onCompleted` fires
|
|
85
|
+
6. `ad.close()` triggers `/complete`
|
|
86
|
+
7. after backend confirmation, `onUserEarnedReward` fires
|
|
87
|
+
|
|
88
|
+
## Endpoint Contract
|
|
89
|
+
|
|
90
|
+
The SDK expects this response from `GET {base}/v1/ad`:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"id": 2,
|
|
95
|
+
"title": "ad-Kotak Bank2",
|
|
96
|
+
"app_name": "SBI Bank",
|
|
97
|
+
"logo_url": "https://.../kotak.png",
|
|
98
|
+
"background_video_url": "https://.../kotak.ts",
|
|
99
|
+
"background_image_url": "https://.../kotak.png",
|
|
100
|
+
"button_text": "Register Now",
|
|
101
|
+
"click_url": "https://play.google.com/store/apps/details?id=...",
|
|
102
|
+
"min_watch_seconds": 5
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The SDK also keeps the existing reward verification calls:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
POST {base}/v1/ads/{adId}/impression
|
|
110
|
+
POST {base}/v1/ads/{adId}/complete
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Live Backend
|
|
114
|
+
|
|
115
|
+
The Android repository defaults to:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
https://myownadsprodapi.hiqmobi.in/api
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
If you want a local fallback, change `USE_STUB` in
|
|
122
|
+
[android/src/main/java/com/ownad/adsdk/AdRepository.kt](android/src/main/java/com/ownad/adsdk/AdRepository.kt).
|
|
123
|
+
|
|
124
|
+
## API Surface
|
|
125
|
+
|
|
126
|
+
- `initialize(appId, config?)`
|
|
127
|
+
- `RewardedAd.createForAdRequest(adUnitId)`
|
|
128
|
+
- `ad.load()`
|
|
129
|
+
- `ad.show()`
|
|
130
|
+
- `ad.close()`
|
|
131
|
+
- `ad.addListener({...})`
|
|
132
|
+
- `ad.destroy()`
|
|
133
|
+
|
|
134
|
+
## Notes
|
|
135
|
+
|
|
136
|
+
- The ad remains non-skippable for the reward flow.
|
|
137
|
+
- The bottom install card uses the response fields directly.
|
|
138
|
+
- `min_watch_seconds` is passed through to native so you can extend the close
|
|
139
|
+
policy later if needed.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'kotlin-android'
|
|
3
|
+
|
|
4
|
+
android {
|
|
5
|
+
compileSdkVersion 37
|
|
6
|
+
|
|
7
|
+
defaultConfig {
|
|
8
|
+
minSdkVersion 24
|
|
9
|
+
targetSdkVersion 36
|
|
10
|
+
versionCode 1
|
|
11
|
+
versionName "1.0.0"
|
|
12
|
+
|
|
13
|
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
buildFeatures {
|
|
17
|
+
buildConfig true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
buildTypes {
|
|
21
|
+
release {
|
|
22
|
+
minifyEnabled true
|
|
23
|
+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
24
|
+
}
|
|
25
|
+
debug {
|
|
26
|
+
minifyEnabled false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
compileOptions {
|
|
31
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
32
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
kotlinOptions {
|
|
36
|
+
jvmTarget = '17'
|
|
37
|
+
freeCompilerArgs += [
|
|
38
|
+
'-opt-in=androidx.media3.common.util.UnstableApi'
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
dependencies {
|
|
44
|
+
implementation 'com.facebook.react:react-android:0.73.0'
|
|
45
|
+
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
|
|
46
|
+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'
|
|
47
|
+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
|
48
|
+
|
|
49
|
+
// Full-screen video ad player
|
|
50
|
+
implementation 'androidx.activity:activity-ktx:1.8.2'
|
|
51
|
+
implementation 'androidx.media3:media3-exoplayer:1.2.1'
|
|
52
|
+
implementation 'androidx.media3:media3-ui:1.2.1'
|
|
53
|
+
|
|
54
|
+
// Testing
|
|
55
|
+
testImplementation 'junit:junit:4.13.2'
|
|
56
|
+
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
|
57
|
+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
|
58
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Keep the SDK classes public
|
|
2
|
+
-keep class com.rozcash.sdk.** { *; }
|
|
3
|
+
|
|
4
|
+
# Keep React Native
|
|
5
|
+
-keep class com.facebook.react.** { *; }
|
|
6
|
+
|
|
7
|
+
# Obfuscate internal implementations
|
|
8
|
+
-renameclasses com.rozcash.sdk.**
|
|
9
|
+
|
|
10
|
+
# Remove logging in release builds
|
|
11
|
+
-assumenosideeffects class android.util.Log {
|
|
12
|
+
public static *** d(...);
|
|
13
|
+
public static *** v(...);
|
|
14
|
+
public static *** i(...);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Keep native methods
|
|
18
|
+
-keepclasseswithmembernames class * {
|
|
19
|
+
native <methods>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
package="com.ownad.adsdk">
|
|
4
|
+
|
|
5
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
6
|
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
7
|
+
|
|
8
|
+
<application>
|
|
9
|
+
<activity
|
|
10
|
+
android:name="com.ownad.adsdk.VideoAdActivity"
|
|
11
|
+
android:configChanges="orientation|screenSize|keyboardHidden|smallestScreenSize|screenLayout|uiMode"
|
|
12
|
+
android:exported="false"
|
|
13
|
+
android:hardwareAccelerated="true"
|
|
14
|
+
android:screenOrientation="portrait"
|
|
15
|
+
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" />
|
|
16
|
+
</application>
|
|
17
|
+
|
|
18
|
+
</manifest>
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
package com.ownad.adsdk
|
|
2
|
+
|
|
3
|
+
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
4
|
+
import okhttp3.MediaType.Companion.toMediaType
|
|
5
|
+
import okhttp3.OkHttpClient
|
|
6
|
+
import okhttp3.Request
|
|
7
|
+
import okhttp3.RequestBody.Companion.toRequestBody
|
|
8
|
+
import org.json.JSONObject
|
|
9
|
+
import java.util.concurrent.TimeUnit
|
|
10
|
+
|
|
11
|
+
/** Ad preview returned by GET /v1/ad. Used only to show the user something is coming. */
|
|
12
|
+
data class AdPreview(
|
|
13
|
+
val id: String,
|
|
14
|
+
val title: String,
|
|
15
|
+
val appName: String,
|
|
16
|
+
val logoUrl: String?,
|
|
17
|
+
val backgroundVideoUrl: String,
|
|
18
|
+
val backgroundImageUrl: String?,
|
|
19
|
+
val buttonText: String,
|
|
20
|
+
val clickUrl: String?,
|
|
21
|
+
val minWatchSeconds: Int,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
/** Authoritative ad + transaction, returned by POST /transactions/. This is what actually plays. */
|
|
25
|
+
data class AdTransaction(
|
|
26
|
+
val txId: String,
|
|
27
|
+
val nonce: String,
|
|
28
|
+
val expiresAt: String?,
|
|
29
|
+
val serverSignature: String?,
|
|
30
|
+
val ad: AdPreview,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Talks to the OwnAd ad server. All calls are blocking and MUST be run off the main thread.
|
|
35
|
+
*
|
|
36
|
+
* Endpoints:
|
|
37
|
+
* GET {base}/v1/ad/ -> ad preview, or { status: "no_fill" }
|
|
38
|
+
* POST {base}/v1/transactions/ (X-App-Id/X-App-Secret)
|
|
39
|
+
* body: { user_id, purpose? }
|
|
40
|
+
* -> { tx_id, nonce, expires_at, server_signature, ad } or { status: "no_fill" }
|
|
41
|
+
* POST {base}/v1/ads/{tx_id}/impression/ -> { ok: true }
|
|
42
|
+
* POST {base}/v1/ads/{tx_id}/progress/ body: { watch_ms } -> { ok: true }
|
|
43
|
+
* POST {base}/v1/ads/{tx_id}/click/ -> { click_url }
|
|
44
|
+
* POST {base}/v1/transactions/{tx_id}/complete/ (X-App-Id/X-App-Secret)
|
|
45
|
+
* body: { nonce } -> { status: "verified" | "rejected" }
|
|
46
|
+
*/
|
|
47
|
+
internal class AdRepository(
|
|
48
|
+
baseUrlOverride: String? = null,
|
|
49
|
+
) {
|
|
50
|
+
companion object {
|
|
51
|
+
private const val AD_BASE_URL = "https://myownadsprodapi.hiqmobi.in/api"
|
|
52
|
+
private val JSON = "application/json; charset=utf-8".toMediaType()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private val baseUrl = (baseUrlOverride ?: AD_BASE_URL).trimEnd('/')
|
|
56
|
+
|
|
57
|
+
private val http = OkHttpClient.Builder()
|
|
58
|
+
.connectTimeout(10, TimeUnit.SECONDS)
|
|
59
|
+
.readTimeout(15, TimeUnit.SECONDS)
|
|
60
|
+
.writeTimeout(10, TimeUnit.SECONDS)
|
|
61
|
+
.build()
|
|
62
|
+
|
|
63
|
+
/** GET /v1/ad/ — ad preview (details only). @return null on "no_fill". */
|
|
64
|
+
fun fetchAdPreview(): AdPreview? {
|
|
65
|
+
val url = baseUrl.toHttpUrl().newBuilder()
|
|
66
|
+
.addEncodedPathSegments("v1/ad/")
|
|
67
|
+
.build()
|
|
68
|
+
|
|
69
|
+
val request = Request.Builder()
|
|
70
|
+
.url(url)
|
|
71
|
+
.get()
|
|
72
|
+
.addHeader("Accept", "application/json")
|
|
73
|
+
.addHeader("User-Agent", "OwnAdSDK/1.0")
|
|
74
|
+
.build()
|
|
75
|
+
|
|
76
|
+
val json = execute(request)
|
|
77
|
+
if (json.optString("status") == "no_fill") return null
|
|
78
|
+
return json.toPreview()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* POST /v1/transactions/ — reserves an ad for this user.
|
|
83
|
+
* @return null on "no_fill".
|
|
84
|
+
*/
|
|
85
|
+
fun startTransaction(
|
|
86
|
+
appId: String,
|
|
87
|
+
appSecret: String,
|
|
88
|
+
userId: String,
|
|
89
|
+
purpose: String?,
|
|
90
|
+
): AdTransaction? {
|
|
91
|
+
val body = JSONObject().put("user_id", userId)
|
|
92
|
+
if (!purpose.isNullOrBlank()) body.put("purpose", purpose)
|
|
93
|
+
|
|
94
|
+
val request = Request.Builder()
|
|
95
|
+
.url(baseUrl + "/v1/transactions/")
|
|
96
|
+
.post(body.toString().toRequestBody(JSON))
|
|
97
|
+
.addHeader("Accept", "application/json")
|
|
98
|
+
.addHeader("User-Agent", "OwnAdSDK/1.0")
|
|
99
|
+
.addHeader("X-App-Id", appId)
|
|
100
|
+
.addHeader("X-App-Secret", appSecret)
|
|
101
|
+
.build()
|
|
102
|
+
|
|
103
|
+
val json = execute(request)
|
|
104
|
+
if (json.optString("status") == "no_fill") return null
|
|
105
|
+
return json.toTransaction()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** POST /v1/ads/{tx_id}/impression/ — fire-and-forget, always 200. */
|
|
109
|
+
fun trackImpression(txId: String) {
|
|
110
|
+
runCatching { execute(postRequest("/v1/ads/$txId/impression/", JSONObject())) }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** POST /v1/ads/{tx_id}/progress/ — server keeps the max watch_ms ever reported. */
|
|
114
|
+
fun trackProgress(txId: String, watchMs: Long) {
|
|
115
|
+
runCatching {
|
|
116
|
+
execute(postRequest("/v1/ads/$txId/progress/", JSONObject().put("watch_ms", watchMs)))
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** POST /v1/ads/{tx_id}/click/ — @return click_url from the server, or null if tx unknown. */
|
|
121
|
+
fun trackClick(txId: String): String? {
|
|
122
|
+
val json = execute(postRequest("/v1/ads/$txId/click/", JSONObject()))
|
|
123
|
+
return json.nullableString("click_url")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* POST /v1/transactions/{tx_id}/complete/ — verifies the nonce and grants the
|
|
128
|
+
* server-side reward (SSV). @return true only when status == "verified".
|
|
129
|
+
*/
|
|
130
|
+
fun completeTransaction(appId: String, appSecret: String, txId: String, nonce: String): Boolean {
|
|
131
|
+
val request = Request.Builder()
|
|
132
|
+
.url(baseUrl + "/v1/transactions/$txId/complete/")
|
|
133
|
+
.post(JSONObject().put("nonce", nonce).toString().toRequestBody(JSON))
|
|
134
|
+
.addHeader("Accept", "application/json")
|
|
135
|
+
.addHeader("User-Agent", "OwnAdSDK/1.0")
|
|
136
|
+
.addHeader("X-App-Id", appId)
|
|
137
|
+
.addHeader("X-App-Secret", appSecret)
|
|
138
|
+
.build()
|
|
139
|
+
|
|
140
|
+
val json = execute(request)
|
|
141
|
+
return json.optString("status") == "verified"
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---- internals --------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
private fun postRequest(path: String, body: JSONObject): Request =
|
|
147
|
+
Request.Builder()
|
|
148
|
+
.url(baseUrl + path)
|
|
149
|
+
.post(body.toString().toRequestBody(JSON))
|
|
150
|
+
.addHeader("Accept", "application/json")
|
|
151
|
+
.addHeader("User-Agent", "OwnAdSDK/1.0")
|
|
152
|
+
.build()
|
|
153
|
+
|
|
154
|
+
private fun execute(request: Request): JSONObject {
|
|
155
|
+
http.newCall(request).execute().use { response ->
|
|
156
|
+
val text = response.body?.string().orEmpty()
|
|
157
|
+
if (!response.isSuccessful) {
|
|
158
|
+
throw AdException("HTTP_${response.code}", "Ad server error ${response.code}: $text")
|
|
159
|
+
}
|
|
160
|
+
return if (text.isBlank()) JSONObject() else JSONObject(text)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private fun JSONObject.toPreview(): AdPreview {
|
|
165
|
+
return AdPreview(
|
|
166
|
+
id = stringOr("id", ""),
|
|
167
|
+
title = stringOr("title", ""),
|
|
168
|
+
appName = stringOr("app_name", ""),
|
|
169
|
+
logoUrl = nullableString("logo_url"),
|
|
170
|
+
backgroundVideoUrl = stringOr("background_video_url", ""),
|
|
171
|
+
backgroundImageUrl = nullableString("background_image_url"),
|
|
172
|
+
buttonText = stringOr("button_text", "Install"),
|
|
173
|
+
clickUrl = nullableString("click_url"),
|
|
174
|
+
minWatchSeconds = optInt("min_watch_seconds", 0),
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private fun JSONObject.toTransaction(): AdTransaction {
|
|
179
|
+
val adJson = optJSONObject("ad") ?: JSONObject()
|
|
180
|
+
return AdTransaction(
|
|
181
|
+
txId = stringOr("tx_id", ""),
|
|
182
|
+
nonce = stringOr("nonce", ""),
|
|
183
|
+
expiresAt = nullableString("expires_at"),
|
|
184
|
+
serverSignature = nullableString("server_signature"),
|
|
185
|
+
ad = adJson.toPreview(),
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private fun JSONObject.stringOr(key: String, fallback: String): String {
|
|
190
|
+
val value = optString(key)
|
|
191
|
+
return if (value.isNullOrBlank() || value == "null") fallback else value
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private fun JSONObject.nullableString(key: String): String? {
|
|
195
|
+
if (!has(key) || isNull(key)) return null
|
|
196
|
+
val value = optString(key)
|
|
197
|
+
return value.takeUnless { it.isBlank() || it == "null" }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
internal class AdException(val code: String, message: String) : Exception(message)
|