react-native-iap 14.4.33 → 14.4.35-rc.1

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.
@@ -194,6 +194,11 @@ dependencies {
194
194
  // Use standard Google Play Billing
195
195
  implementation "io.github.hyochan.openiap:openiap-google:${googleVersionString}"
196
196
  }
197
+
198
+ // Test dependencies
199
+ testImplementation 'junit:junit:4.13.2'
200
+ testImplementation 'org.jetbrains.kotlin:kotlin-test'
201
+ testImplementation 'org.jetbrains.kotlin:kotlin-test-junit'
197
202
  }
198
203
 
199
204
  configurations.all {
@@ -37,6 +37,22 @@ import org.json.JSONArray
37
37
  import org.json.JSONObject
38
38
  import java.util.Locale
39
39
 
40
+ /**
41
+ * Custom exception for OpenIAP errors that only includes the error JSON without stack traces.
42
+ * This ensures clean error messages are passed to JavaScript without Java/Kotlin stack traces.
43
+ */
44
+ class OpenIapException(private val errorJson: String) : Exception() {
45
+ override val message: String
46
+ get() = errorJson
47
+
48
+ override fun toString(): String = errorJson
49
+
50
+ override fun fillInStackTrace(): Throwable {
51
+ // Don't fill in stack trace to avoid it being serialized
52
+ return this
53
+ }
54
+ }
55
+
40
56
  class HybridRnIap : HybridRnIapSpec() {
41
57
 
42
58
  // Get ReactApplicationContext lazily from NitroModules
@@ -167,7 +183,7 @@ class HybridRnIap : HybridRnIapSpec() {
167
183
  } catch (err: Throwable) {
168
184
  val error = OpenIAPError.InitConnection
169
185
  RnIapLog.failure("initConnection.native", err)
170
- throw Exception(
186
+ throw OpenIapException(
171
187
  toErrorJson(
172
188
  error = error,
173
189
  debugMessage = err.message,
@@ -178,7 +194,7 @@ class HybridRnIap : HybridRnIapSpec() {
178
194
  if (!ok) {
179
195
  val error = OpenIAPError.InitConnection
180
196
  RnIapLog.failure("initConnection.native", Exception(error.message))
181
- throw Exception(
197
+ throw OpenIapException(
182
198
  toErrorJson(
183
199
  error = error,
184
200
  messageOverride = "Failed to initialize connection"
@@ -225,7 +241,7 @@ class HybridRnIap : HybridRnIapSpec() {
225
241
  )
226
242
 
227
243
  if (skus.isEmpty()) {
228
- throw Exception(toErrorJson(OpenIAPError.EmptySkuList))
244
+ throw OpenIapException(toErrorJson(OpenIAPError.EmptySkuList))
229
245
  }
230
246
 
231
247
  initConnection(null).await()
@@ -528,7 +544,7 @@ class HybridRnIap : HybridRnIapSpec() {
528
544
  } catch (e: Exception) {
529
545
  RnIapLog.failure("getActiveSubscriptions", e)
530
546
  val error = OpenIAPError.ServiceUnavailable
531
- throw Exception(
547
+ throw OpenIapException(
532
548
  toErrorJson(
533
549
  error = error,
534
550
  debugMessage = e.message,
@@ -937,14 +953,14 @@ class HybridRnIap : HybridRnIapSpec() {
937
953
  // iOS-specific method - not supported on Android
938
954
  override fun getStorefrontIOS(): Promise<String> {
939
955
  return Promise.async {
940
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
956
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
941
957
  }
942
958
  }
943
959
 
944
960
  // iOS-specific method - not supported on Android
945
961
  override fun getAppTransactionIOS(): Promise<String?> {
946
962
  return Promise.async {
947
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
963
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
948
964
  }
949
965
  }
950
966
 
@@ -1031,7 +1047,7 @@ class HybridRnIap : HybridRnIapSpec() {
1031
1047
  try {
1032
1048
  // For Android, we need the androidOptions to be provided
1033
1049
  val androidOptions = params.androidOptions
1034
- ?: throw Exception(toErrorJson(OpenIAPError.DeveloperError))
1050
+ ?: throw OpenIapException(toErrorJson(OpenIAPError.DeveloperError))
1035
1051
 
1036
1052
  // Android receipt validation would typically involve server-side validation
1037
1053
  // using Google Play Developer API. Here we provide a simplified implementation
@@ -1070,7 +1086,7 @@ class HybridRnIap : HybridRnIapSpec() {
1070
1086
  } catch (e: Exception) {
1071
1087
  val debugMessage = e.message
1072
1088
  val error = OpenIAPError.InvalidReceipt
1073
- throw Exception(
1089
+ throw OpenIapException(
1074
1090
  toErrorJson(
1075
1091
  error = error,
1076
1092
  debugMessage = debugMessage,
@@ -1084,31 +1100,31 @@ class HybridRnIap : HybridRnIapSpec() {
1084
1100
  // iOS-specific methods - Not applicable on Android, return appropriate defaults
1085
1101
  override fun subscriptionStatusIOS(sku: String): Promise<Array<NitroSubscriptionStatus>?> {
1086
1102
  return Promise.async {
1087
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
1103
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
1088
1104
  }
1089
1105
  }
1090
1106
 
1091
1107
  override fun currentEntitlementIOS(sku: String): Promise<NitroPurchase?> {
1092
1108
  return Promise.async {
1093
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
1109
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
1094
1110
  }
1095
1111
  }
1096
1112
 
1097
1113
  override fun latestTransactionIOS(sku: String): Promise<NitroPurchase?> {
1098
1114
  return Promise.async {
1099
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
1115
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
1100
1116
  }
1101
1117
  }
1102
1118
 
1103
1119
  override fun getPendingTransactionsIOS(): Promise<Array<NitroPurchase>> {
1104
1120
  return Promise.async {
1105
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
1121
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
1106
1122
  }
1107
1123
  }
1108
1124
 
1109
1125
  override fun syncIOS(): Promise<Boolean> {
1110
1126
  return Promise.async {
1111
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
1127
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
1112
1128
  }
1113
1129
  }
1114
1130
 
@@ -1116,37 +1132,37 @@ class HybridRnIap : HybridRnIapSpec() {
1116
1132
 
1117
1133
  override fun isEligibleForIntroOfferIOS(groupID: String): Promise<Boolean> {
1118
1134
  return Promise.async {
1119
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
1135
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
1120
1136
  }
1121
1137
  }
1122
1138
 
1123
1139
  override fun getReceiptDataIOS(): Promise<String> {
1124
1140
  return Promise.async {
1125
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
1141
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
1126
1142
  }
1127
1143
  }
1128
1144
 
1129
1145
  override fun getReceiptIOS(): Promise<String> {
1130
1146
  return Promise.async {
1131
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
1147
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
1132
1148
  }
1133
1149
  }
1134
1150
 
1135
1151
  override fun requestReceiptRefreshIOS(): Promise<String> {
1136
1152
  return Promise.async {
1137
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
1153
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
1138
1154
  }
1139
1155
  }
1140
1156
 
1141
1157
  override fun isTransactionVerifiedIOS(sku: String): Promise<Boolean> {
1142
1158
  return Promise.async {
1143
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
1159
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
1144
1160
  }
1145
1161
  }
1146
1162
 
1147
1163
  override fun getTransactionJwsIOS(sku: String): Promise<String?> {
1148
1164
  return Promise.async {
1149
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
1165
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
1150
1166
  }
1151
1167
  }
1152
1168
 
@@ -1166,7 +1182,7 @@ class HybridRnIap : HybridRnIapSpec() {
1166
1182
  } catch (err: Throwable) {
1167
1183
  RnIapLog.failure("checkAlternativeBillingAvailabilityAndroid", err)
1168
1184
  val errorType = parseOpenIapError(err)
1169
- throw Exception(toErrorJson(errorType, debugMessage = err.message))
1185
+ throw OpenIapException(toErrorJson(errorType, debugMessage = err.message))
1170
1186
  }
1171
1187
  }
1172
1188
  }
@@ -1176,7 +1192,7 @@ class HybridRnIap : HybridRnIapSpec() {
1176
1192
  RnIapLog.payload("showAlternativeBillingDialogAndroid", null)
1177
1193
  try {
1178
1194
  val activity = context.currentActivity
1179
- ?: throw Exception(toErrorJson(OpenIAPError.DeveloperError, debugMessage = "Activity not available"))
1195
+ ?: throw OpenIapException(toErrorJson(OpenIAPError.DeveloperError, debugMessage = "Activity not available"))
1180
1196
 
1181
1197
  val userAccepted = withContext(Dispatchers.Main) {
1182
1198
  openIap.setActivity(activity)
@@ -1187,7 +1203,7 @@ class HybridRnIap : HybridRnIapSpec() {
1187
1203
  } catch (err: Throwable) {
1188
1204
  RnIapLog.failure("showAlternativeBillingDialogAndroid", err)
1189
1205
  val errorType = parseOpenIapError(err)
1190
- throw Exception(toErrorJson(errorType, debugMessage = err.message))
1206
+ throw OpenIapException(toErrorJson(errorType, debugMessage = err.message))
1191
1207
  }
1192
1208
  }
1193
1209
  }
@@ -1206,7 +1222,7 @@ class HybridRnIap : HybridRnIapSpec() {
1206
1222
  } catch (err: Throwable) {
1207
1223
  RnIapLog.failure("createAlternativeBillingTokenAndroid", err)
1208
1224
  val errorType = parseOpenIapError(err)
1209
- throw Exception(toErrorJson(errorType, debugMessage = err.message))
1225
+ throw OpenIapException(toErrorJson(errorType, debugMessage = err.message))
1210
1226
  }
1211
1227
  }
1212
1228
  }
@@ -1236,19 +1252,19 @@ class HybridRnIap : HybridRnIapSpec() {
1236
1252
 
1237
1253
  override fun canPresentExternalPurchaseNoticeIOS(): Promise<Boolean> {
1238
1254
  return Promise.async {
1239
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
1255
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
1240
1256
  }
1241
1257
  }
1242
1258
 
1243
1259
  override fun presentExternalPurchaseNoticeSheetIOS(): Promise<ExternalPurchaseNoticeResultIOS> {
1244
1260
  return Promise.async {
1245
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
1261
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
1246
1262
  }
1247
1263
  }
1248
1264
 
1249
1265
  override fun presentExternalPurchaseLinkIOS(url: String): Promise<ExternalPurchaseLinkResultIOS> {
1250
1266
  return Promise.async {
1251
- throw Exception(toErrorJson(OpenIAPError.FeatureNotSupported))
1267
+ throw OpenIapException(toErrorJson(OpenIAPError.FeatureNotSupported))
1252
1268
  }
1253
1269
  }
1254
1270
 
@@ -0,0 +1,93 @@
1
+ package com.margelo.nitro.iap
2
+
3
+ import org.junit.Test
4
+ import org.junit.Assert.*
5
+
6
+ /**
7
+ * Unit tests for OpenIapException to verify that error messages
8
+ * are returned without Java/Kotlin stack traces.
9
+ *
10
+ * This addresses Issue #3075 where error.code was undefined because
11
+ * stack traces were breaking JSON parsing on the JavaScript side.
12
+ */
13
+ class OpenIapExceptionTest {
14
+
15
+ @Test
16
+ fun `test OpenIapException message returns clean JSON`() {
17
+ val errorJson = """{"code":"init-connection","message":"Failed to initialize connection"}"""
18
+ val exception = OpenIapException(errorJson)
19
+
20
+ // Verify message is exactly the JSON string
21
+ assertEquals(errorJson, exception.message)
22
+ }
23
+
24
+ @Test
25
+ fun `test OpenIapException toString returns clean JSON without stack trace`() {
26
+ val errorJson = """{"code":"user-cancelled","message":"User cancelled"}"""
27
+ val exception = OpenIapException(errorJson)
28
+
29
+ // toString() should return only the JSON, not "java.lang.Exception: ..."
30
+ val result = exception.toString()
31
+ assertEquals(errorJson, result)
32
+ assertFalse(result.startsWith("java.lang.Exception:"))
33
+ assertFalse(result.contains("\tat "))
34
+ }
35
+
36
+ @Test
37
+ fun `test thrown OpenIapException message is clean`() {
38
+ val errorJson = """{"code":"network-error","message":"Network error occurred","responseCode":-1}"""
39
+
40
+ try {
41
+ throw OpenIapException(errorJson)
42
+ } catch (e: Exception) {
43
+ // Verify the caught exception message is clean
44
+ assertEquals(errorJson, e.message)
45
+ assertEquals(errorJson, e.toString())
46
+ }
47
+ }
48
+
49
+ @Test
50
+ fun `test OpenIapException with complex JSON structure`() {
51
+ val errorJson = """{"code":"purchase-error","message":"Purchase failed","responseCode":3,"debugMessage":"Item unavailable","productId":"com.test.product"}"""
52
+ val exception = OpenIapException(errorJson)
53
+
54
+ assertEquals(errorJson, exception.message)
55
+ assertEquals(errorJson, exception.toString())
56
+ }
57
+
58
+ @Test
59
+ fun `test multiple OpenIapException instances are independent`() {
60
+ val error1 = """{"code":"error-1","message":"First error"}"""
61
+ val error2 = """{"code":"error-2","message":"Second error"}"""
62
+
63
+ val exception1 = OpenIapException(error1)
64
+ val exception2 = OpenIapException(error2)
65
+
66
+ assertEquals(error1, exception1.message)
67
+ assertEquals(error2, exception2.message)
68
+ assertNotEquals(exception1.message, exception2.message)
69
+ }
70
+
71
+ @Test
72
+ fun `test OpenIapException with empty JSON`() {
73
+ val errorJson = """{}"""
74
+ val exception = OpenIapException(errorJson)
75
+
76
+ assertEquals(errorJson, exception.message)
77
+ assertEquals(errorJson, exception.toString())
78
+ }
79
+
80
+ @Test
81
+ fun `test OpenIapException message does not contain stack trace keywords`() {
82
+ val errorJson = """{"code":"test-error","message":"Test message"}"""
83
+ val exception = OpenIapException(errorJson)
84
+
85
+ val result = exception.toString()
86
+
87
+ // Verify no stack trace keywords are present
88
+ assertFalse("Should not contain 'at ' (stack trace)", result.contains("\tat "))
89
+ assertFalse("Should not contain 'java.lang.Exception:'", result.contains("java.lang.Exception:"))
90
+ assertFalse("Should not contain '.kt:' (Kotlin file reference)", result.contains(".kt:"))
91
+ assertFalse("Should not contain '.java:' (Java file reference)", result.contains(".java:"))
92
+ }
93
+ }
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <Workspace
3
+ version = "1.0">
4
+ <FileRef
5
+ location = "self:">
6
+ </FileRef>
7
+ </Workspace>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>IDEDidComputeMac32BitWarning</key>
6
+ <true/>
7
+ </dict>
8
+ </plist>