Skip to content
Merged
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package io.github.hyochan.kmpiap

import io.github.hyochan.kmpiap.openiap.ErrorCode
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import platform.Foundation.NSError
import platform.Foundation.NSNull

class IosErrorMappingTest {
@Test
fun testNSErrorUserInfoMapsToPurchaseException() {
val error = NSError.errorWithDomain(
domain = "OpenIAP",
code = -1,
userInfo = mapOf(
"code" to "user-cancelled",
"message" to "Request Canceled",
"productId" to "premium_monthly",
"debugMessage" to "StoreKit cancellation"
)
)

val exception = error.toPurchaseException()

assertEquals(ErrorCode.UserCancelled, exception.error.code)
assertEquals("Request Canceled", exception.error.message)
assertEquals("premium_monthly", exception.error.productId)
assertEquals("StoreKit cancellation", exception.error.debugMessage)
}

@Test
fun testNSErrorCodeFallback() {
val error = NSError.errorWithDomain(
domain = "OpenIAP",
code = -1,
userInfo = mapOf("message" to "Native failure")
)

val exception = error.toPurchaseException(ErrorCode.ServiceError)

assertEquals(ErrorCode.ServiceError, exception.error.code)
assertEquals("Native failure", exception.error.message)
}

@Test
fun testNSErrorUserInfoIgnoresNSNullValues() {
val error = NSError.errorWithDomain(
domain = "OpenIAP",
code = -1,
userInfo = mapOf(
"code" to "user-cancelled",
"message" to "Request Canceled",
"debugMessage" to NSNull()
)
)

val exception = error.toPurchaseException()

assertEquals(ErrorCode.UserCancelled, exception.error.code)
assertEquals("Request Canceled", exception.error.message)
assertNull(exception.error.debugMessage)
}
}
17 changes: 17 additions & 0 deletions libraries/react-native-iap/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,23 @@ describe('Public API (src/index.ts)', () => {
await expect(IAP.syncIOS()).resolves.toBe(true);
});

it('syncIOS preserves Nitro user cancellation without error logging', async () => {
(Platform as any).OS = 'ios';
mockIap.syncIOS = jest.fn(async () => {
throw new Error(
'Error Domain=com.margelo.nitro.rniap Code=-1 ' +
'"{\\"message\\":\\"Request Canceled\\",\\"code\\":\\"user-cancelled\\"}" ' +
'UserInfo={NSLocalizedDescription={\\"message\\":\\"Request Canceled\\",\\"code\\":\\"user-cancelled\\"}}',
);
});

await expect(IAP.syncIOS()).rejects.toMatchObject({
code: ErrorCode.UserCancelled,
message: 'Request Canceled',
});
expect(console.error).not.toHaveBeenCalled();
});

it('restorePurchases on iOS calls syncIOS first', async () => {
(Platform as any).OS = 'ios';
mockIap.syncIOS = jest.fn(async () => true);
Expand Down
102 changes: 102 additions & 0 deletions libraries/react-native-iap/src/__tests__/utils/error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,108 @@ describe('Error utilities', () => {
});
});

it('should preserve structured code from Error objects', () => {
const error = Object.assign(new Error('Request Canceled'), {
code: ErrorCode.UserCancelled,
});

const result = parseErrorStringToJsonObj(error);

expect(result).toEqual({
code: ErrorCode.UserCancelled,
message: 'Request Canceled',
});
});

it('should preserve structured fields from Error objects', () => {
const error = Object.assign(new Error('Request Canceled'), {
code: ErrorCode.UserCancelled,
productId: 'premium_monthly',
responseCode: 1,
});

const result = parseErrorStringToJsonObj(error);

expect(result).toEqual({
code: ErrorCode.UserCancelled,
message: 'Request Canceled',
productId: 'premium_monthly',
responseCode: 1,
});
});

it('should parse Nitro NSError string with embedded JSON payload', () => {
const error = new Error(
'Error Domain=com.margelo.nitro.rniap Code=-1 ' +
'"{\\"message\\":\\"Request Canceled\\",\\"code\\":\\"user-cancelled\\"}" ' +
'UserInfo={NSLocalizedDescription={\\"message\\":\\"Request Canceled\\",\\"code\\":\\"user-cancelled\\"}}',
);

const result = parseErrorStringToJsonObj(error);

expect(result).toEqual({
code: ErrorCode.UserCancelled,
message: 'Request Canceled',
});
expect(isUserCancelledError(error)).toBe(true);
});

it('should parse quoted NSError JSON without UserInfo', () => {
const error = new Error(
'Error Domain=com.margelo.nitro.rniap Code=-1 ' +
'"{\\"message\\":\\"Request Canceled\\",\\"code\\":\\"user-cancelled\\"}"',
);

const result = parseErrorStringToJsonObj(error);

expect(result).toEqual({
code: ErrorCode.UserCancelled,
message: 'Request Canceled',
});
});

it('should parse escaped UserInfo JSON when NSError quoted payload is plain text', () => {
const error = new Error(
'Error Domain=SKErrorDomain Code=-1 "The operation failed." ' +
'UserInfo={NSLocalizedDescription={\\"message\\":\\"Request Canceled\\",\\"code\\":\\"user-cancelled\\"}}',
);

const result = parseErrorStringToJsonObj(error);

expect(result).toEqual({
code: ErrorCode.UserCancelled,
message: 'Request Canceled',
});
});

it('should parse escaped UserInfo JSON with braces in message', () => {
const error = new Error(
'Error Domain=SKErrorDomain Code=-1 "The operation failed." ' +
'UserInfo={NSLocalizedDescription={\\"message\\":\\"Request {Canceled}\\",\\"code\\":\\"user-cancelled\\"}}',
);

const result = parseErrorStringToJsonObj(error);

expect(result).toEqual({
code: ErrorCode.UserCancelled,
message: 'Request {Canceled}',
});
});

it('should parse quoted localized description JSON', () => {
const error = new Error(
'Error Domain=SKErrorDomain Code=-1 "The operation failed." ' +
'UserInfo={NSLocalizedDescription="{\\"message\\":\\"Request Canceled\\",\\"code\\":\\"user-cancelled\\"}"}',
);

const result = parseErrorStringToJsonObj(error);

expect(result).toEqual({
code: ErrorCode.UserCancelled,
message: 'Request Canceled',
});
});

it('should handle non-JSON string', () => {
const errorString = 'Not a JSON string';

Expand Down
Loading
Loading