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 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.
File without changes
@@ -0,0 +1,2 @@
1
+ #Fri Aug 28 17:37:40 IST 2026
2
+ gradle.version=9.2.0
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)