From 7201173564cccc7182fa226bc7596a75a959404b Mon Sep 17 00:00:00 2001 From: ignaciosantise <25931366+ignaciosantise@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:07:31 -0300 Subject: [PATCH 1/6] fix(core): load wallet & network images on React Native FetchUtil.fetchImage built its data URL with response.blob() + FileReader, but on React Native fetch(...).blob() throws "Creating blobs from 'ArrayBuffer' and 'ArrayBufferView' are not supported" for binary responses, so wallet and network images silently failed to load. Read the bytes via response.arrayBuffer() and base64-encode them into the data URL instead (dependency-free encoder, since RN guarantees neither Buffer nor btoa). Adds unit tests for fetchImage. Co-Authored-By: Claude Opus 4.8 --- .../fix-fetch-image-blob-arraybuffer.md | 12 ++++ .../src/__tests__/utils/FetchUtil.test.ts | 57 +++++++++++++++++++ packages/core/src/utils/FetchUtil.ts | 37 ++++++++++-- 3 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 .changeset/fix-fetch-image-blob-arraybuffer.md diff --git a/.changeset/fix-fetch-image-blob-arraybuffer.md b/.changeset/fix-fetch-image-blob-arraybuffer.md new file mode 100644 index 000000000..7d4659396 --- /dev/null +++ b/.changeset/fix-fetch-image-blob-arraybuffer.md @@ -0,0 +1,12 @@ +--- +'@reown/appkit-core-react-native': patch +--- + +fix(core): load wallet & network images on React Native + +`FetchUtil.fetchImage` built its data URL with `response.blob()` + `FileReader`, +but on React Native `fetch(...).blob()` throws "Creating blobs from 'ArrayBuffer' +and 'ArrayBufferView' are not supported" for binary responses — so wallet and +network images silently failed to load (placeholders only). Read the bytes via +`response.arrayBuffer()` and base64-encode them into the data URL instead, using +a dependency-free encoder (RN guarantees neither `Buffer` nor `btoa`). diff --git a/packages/core/src/__tests__/utils/FetchUtil.test.ts b/packages/core/src/__tests__/utils/FetchUtil.test.ts index 0e864fb4d..b43002538 100644 --- a/packages/core/src/__tests__/utils/FetchUtil.test.ts +++ b/packages/core/src/__tests__/utils/FetchUtil.test.ts @@ -104,4 +104,61 @@ describe('FetchUtil', () => { expect(url).toBe('https://another.com/test?foo=bar&bar=baz&clientId=test-client-id'); }); }); + + describe('fetchImage', () => { + const originalFetch = global.fetch; + + afterEach(() => { + global.fetch = originalFetch; + }); + + it('should build a base64 data URL from the response ArrayBuffer', async () => { + // "foobar" -> base64 "Zm9vYmFy" + const bytes = new Uint8Array([0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]); + global.fetch = jest.fn().mockResolvedValue({ + arrayBuffer: async () => bytes.buffer, + headers: { get: () => 'image/png' } + }) as unknown as typeof fetch; + + const fetchUtil = new FetchUtil({ baseUrl }); + const result = await fetchUtil.fetchImage('/getWalletImage/1'); + + expect(result).toBe('data:image/png;base64,Zm9vYmFy'); + }); + + it('should default the content type to image/png when the header is missing', async () => { + global.fetch = jest.fn().mockResolvedValue({ + arrayBuffer: async () => new Uint8Array([0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]).buffer, + headers: { get: () => null } + }) as unknown as typeof fetch; + + const fetchUtil = new FetchUtil({ baseUrl }); + const result = await fetchUtil.fetchImage('/getWalletImage/1'); + + expect(result).toBe('data:image/png;base64,Zm9vYmFy'); + }); + + it('should not call response.blob() (RN cannot build a Blob from an ArrayBuffer)', async () => { + const blob = jest.fn(); + global.fetch = jest.fn().mockResolvedValue({ + arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer, + headers: { get: () => 'image/webp' }, + blob + }) as unknown as typeof fetch; + + const fetchUtil = new FetchUtil({ baseUrl }); + await fetchUtil.fetchImage('/getWalletImage/1'); + + expect(blob).not.toHaveBeenCalled(); + }); + + it('should return undefined when the request throws', async () => { + global.fetch = jest.fn().mockRejectedValue(new Error('network error')) as unknown as typeof fetch; + + const fetchUtil = new FetchUtil({ baseUrl }); + const result = await fetchUtil.fetchImage('/getWalletImage/1'); + + expect(result).toBeUndefined(); + }); + }); }); diff --git a/packages/core/src/utils/FetchUtil.ts b/packages/core/src/utils/FetchUtil.ts index f0b3451c2..1a69f95d1 100644 --- a/packages/core/src/utils/FetchUtil.ts +++ b/packages/core/src/utils/FetchUtil.ts @@ -80,18 +80,43 @@ export class FetchUtil { try { const url = this.createUrl({ path, params }).toString(); const response = await fetch(url, { headers }); - const blob = await response.blob(); - const reader = new FileReader(); - reader.readAsDataURL(blob); - return new Promise(resolve => { - reader.onloadend = () => resolve(reader.result as string); - }); + // React Native's `fetch(...).blob()` throws "Creating blobs from + // 'ArrayBuffer' and 'ArrayBufferView' are not supported" for binary + // responses, so Blob + FileReader can't be used to build the data URL + // (wallet/network images would silently fail to load). Read the bytes as + // an ArrayBuffer and base64-encode them into a data URL instead. + const arrayBuffer = await response.arrayBuffer(); + const contentType = response.headers.get('content-type') ?? 'image/png'; + + return `data:${contentType};base64,${FetchUtil._arrayBufferToBase64(arrayBuffer)}`; } catch { return undefined; } } + // Dependency-free base64 encoder (RN has no global `Buffer`/`btoa` guarantee). + /* eslint-disable no-bitwise */ + private static _arrayBufferToBase64(buffer: ArrayBuffer): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + const bytes = new Uint8Array(buffer); + let base64 = ''; + + for (let i = 0; i < bytes.length; i += 3) { + const byte0 = bytes[i] as number; + const byte1 = i + 1 < bytes.length ? (bytes[i + 1] as number) : 0; + const byte2 = i + 2 < bytes.length ? (bytes[i + 2] as number) : 0; + + base64 += chars[byte0 >> 2]; + base64 += chars[((byte0 & 0x03) << 4) | (byte1 >> 4)]; + base64 += i + 1 < bytes.length ? chars[((byte1 & 0x0f) << 2) | (byte2 >> 6)] : '='; + base64 += i + 2 < bytes.length ? chars[byte2 & 0x3f] : '='; + } + + return base64; + } + /* eslint-enable no-bitwise */ + public createUrl({ path, params }: RequestArguments) { let fullUrl: string; From ee0c46625ea805b290b7d293f3ac141b9acfceb5 Mon Sep 17 00:00:00 2001 From: ignaciosantise <25931366+ignaciosantise@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:35:55 -0300 Subject: [PATCH 2/6] chore(core): fix prettier formatting in FetchUtil test Co-Authored-By: Claude Opus 4.8 --- packages/core/src/__tests__/utils/FetchUtil.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/__tests__/utils/FetchUtil.test.ts b/packages/core/src/__tests__/utils/FetchUtil.test.ts index b43002538..49e08b8da 100644 --- a/packages/core/src/__tests__/utils/FetchUtil.test.ts +++ b/packages/core/src/__tests__/utils/FetchUtil.test.ts @@ -153,7 +153,9 @@ describe('FetchUtil', () => { }); it('should return undefined when the request throws', async () => { - global.fetch = jest.fn().mockRejectedValue(new Error('network error')) as unknown as typeof fetch; + global.fetch = jest + .fn() + .mockRejectedValue(new Error('network error')) as unknown as typeof fetch; const fetchUtil = new FetchUtil({ baseUrl }); const result = await fetchUtil.fetchImage('/getWalletImage/1'); From 5600127a62953c7dc19382839615c151455cefc6 Mon Sep 17 00:00:00 2001 From: ignaciosantise <25931366+ignaciosantise@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:47:36 -0300 Subject: [PATCH 3/6] chore(core): bump all packages in changeset Match the release convention of bumping every published package on each release, consistent with the other changeset on this branch and prior releases. Co-Authored-By: Claude Opus 4.8 --- .changeset/fix-fetch-image-blob-arraybuffer.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.changeset/fix-fetch-image-blob-arraybuffer.md b/.changeset/fix-fetch-image-blob-arraybuffer.md index 7d4659396..34bef3dc3 100644 --- a/.changeset/fix-fetch-image-blob-arraybuffer.md +++ b/.changeset/fix-fetch-image-blob-arraybuffer.md @@ -1,5 +1,13 @@ --- +'@reown/appkit-react-native': patch +'@reown/appkit-bitcoin-react-native': patch +'@reown/appkit-coinbase-react-native': patch +'@reown/appkit-common-react-native': patch '@reown/appkit-core-react-native': patch +'@reown/appkit-ethers-react-native': patch +'@reown/appkit-solana-react-native': patch +'@reown/appkit-ui-react-native': patch +'@reown/appkit-wagmi-react-native': patch --- fix(core): load wallet & network images on React Native From c300bde4160fb1952f3b5e0b2df2c2b14977c5c0 Mon Sep 17 00:00:00 2001 From: ignaciosantise <25931366+ignaciosantise@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:38:23 -0300 Subject: [PATCH 4/6] fix(core): guard fetchImage on response.ok and reduce base64 allocations - Return undefined for non-2xx responses so error bodies aren't base64-encoded into a bogus data URL that RN's Image silently fails to render instead of falling back to the placeholder. - Accumulate base64 output into an array and join once to avoid per-iteration string allocations / GC pressure on the RN JS thread. - Add ok: true to the fetchImage success mocks and a non-ok test case. Co-Authored-By: Claude Opus 4.8 --- .../src/__tests__/utils/FetchUtil.test.ts | 18 +++++++++++++++++ packages/core/src/utils/FetchUtil.ts | 20 +++++++++++++------ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/core/src/__tests__/utils/FetchUtil.test.ts b/packages/core/src/__tests__/utils/FetchUtil.test.ts index 49e08b8da..ded3fa795 100644 --- a/packages/core/src/__tests__/utils/FetchUtil.test.ts +++ b/packages/core/src/__tests__/utils/FetchUtil.test.ts @@ -116,6 +116,7 @@ describe('FetchUtil', () => { // "foobar" -> base64 "Zm9vYmFy" const bytes = new Uint8Array([0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]); global.fetch = jest.fn().mockResolvedValue({ + ok: true, arrayBuffer: async () => bytes.buffer, headers: { get: () => 'image/png' } }) as unknown as typeof fetch; @@ -126,8 +127,24 @@ describe('FetchUtil', () => { expect(result).toBe('data:image/png;base64,Zm9vYmFy'); }); + it('should return undefined for a non-ok response', async () => { + const arrayBuffer = jest.fn(); + global.fetch = jest.fn().mockResolvedValue({ + ok: false, + arrayBuffer, + headers: { get: () => 'application/json' } + }) as unknown as typeof fetch; + + const fetchUtil = new FetchUtil({ baseUrl }); + const result = await fetchUtil.fetchImage('/getWalletImage/1'); + + expect(result).toBeUndefined(); + expect(arrayBuffer).not.toHaveBeenCalled(); + }); + it('should default the content type to image/png when the header is missing', async () => { global.fetch = jest.fn().mockResolvedValue({ + ok: true, arrayBuffer: async () => new Uint8Array([0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]).buffer, headers: { get: () => null } }) as unknown as typeof fetch; @@ -141,6 +158,7 @@ describe('FetchUtil', () => { it('should not call response.blob() (RN cannot build a Blob from an ArrayBuffer)', async () => { const blob = jest.fn(); global.fetch = jest.fn().mockResolvedValue({ + ok: true, arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer, headers: { get: () => 'image/webp' }, blob diff --git a/packages/core/src/utils/FetchUtil.ts b/packages/core/src/utils/FetchUtil.ts index 1a69f95d1..d932f7658 100644 --- a/packages/core/src/utils/FetchUtil.ts +++ b/packages/core/src/utils/FetchUtil.ts @@ -81,6 +81,12 @@ export class FetchUtil { const url = this.createUrl({ path, params }).toString(); const response = await fetch(url, { headers }); + // Bail on non-2xx so error bodies aren't base64-encoded into a bogus data + // URL (RN's `Image` would silently fail instead of showing the placeholder). + if (!response.ok) { + return undefined; + } + // React Native's `fetch(...).blob()` throws "Creating blobs from // 'ArrayBuffer' and 'ArrayBufferView' are not supported" for binary // responses, so Blob + FileReader can't be used to build the data URL @@ -100,20 +106,22 @@ export class FetchUtil { private static _arrayBufferToBase64(buffer: ArrayBuffer): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; const bytes = new Uint8Array(buffer); - let base64 = ''; + const parts: string[] = []; for (let i = 0; i < bytes.length; i += 3) { const byte0 = bytes[i] as number; const byte1 = i + 1 < bytes.length ? (bytes[i + 1] as number) : 0; const byte2 = i + 2 < bytes.length ? (bytes[i + 2] as number) : 0; - base64 += chars[byte0 >> 2]; - base64 += chars[((byte0 & 0x03) << 4) | (byte1 >> 4)]; - base64 += i + 1 < bytes.length ? chars[((byte1 & 0x0f) << 2) | (byte2 >> 6)] : '='; - base64 += i + 2 < bytes.length ? chars[byte2 & 0x3f] : '='; + parts.push(chars[byte0 >> 2] as string); + parts.push(chars[((byte0 & 0x03) << 4) | (byte1 >> 4)] as string); + parts.push( + i + 1 < bytes.length ? (chars[((byte1 & 0x0f) << 2) | (byte2 >> 6)] as string) : '=' + ); + parts.push(i + 2 < bytes.length ? (chars[byte2 & 0x3f] as string) : '='); } - return base64; + return parts.join(''); } /* eslint-enable no-bitwise */ From 74b17e7cf1500d1e6df9b689dda0b39b7ce0f952 Mon Sep 17 00:00:00 2001 From: ignaciosantise <25931366+ignaciosantise@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:52:28 -0300 Subject: [PATCH 5/6] fix(core): strip content-type params from fetchImage data URLs Embed only the bare media type in the data URL so responses like `image/svg+xml; charset=utf-8` don't produce a malformed `data:image/svg+xml; charset=utf-8;base64,...` that strict parsers reject. Also add test coverage for the base64 one-/two-byte padding branches (previously every fetchImage test used a byte length divisible by 3, so the `=`/`==` padding was never exercised). Co-Authored-By: Claude Opus 4.8 --- .changeset/fix-fetch-image-base64-padding.md | 19 +++++++++ .../src/__tests__/utils/FetchUtil.test.ts | 41 +++++++++++++++++++ packages/core/src/utils/FetchUtil.ts | 6 ++- 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-fetch-image-base64-padding.md diff --git a/.changeset/fix-fetch-image-base64-padding.md b/.changeset/fix-fetch-image-base64-padding.md new file mode 100644 index 000000000..f104456c0 --- /dev/null +++ b/.changeset/fix-fetch-image-base64-padding.md @@ -0,0 +1,19 @@ +--- +'@reown/appkit-react-native': patch +'@reown/appkit-bitcoin-react-native': patch +'@reown/appkit-coinbase-react-native': patch +'@reown/appkit-common-react-native': patch +'@reown/appkit-core-react-native': patch +'@reown/appkit-ethers-react-native': patch +'@reown/appkit-solana-react-native': patch +'@reown/appkit-ui-react-native': patch +'@reown/appkit-wagmi-react-native': patch +--- + +fix(core): strip content-type parameters from fetchImage data URLs + +`FetchUtil.fetchImage` embedded the raw `content-type` header in the data URL, so +a response like `image/svg+xml; charset=utf-8` produced a malformed +`data:image/svg+xml; charset=utf-8;base64,...` that strict parsers may reject. +Strip semicolon-delimited parameters before building the data URL, and add test +coverage for the base64 one-/two-byte padding branches. diff --git a/packages/core/src/__tests__/utils/FetchUtil.test.ts b/packages/core/src/__tests__/utils/FetchUtil.test.ts index ded3fa795..52e0a2e88 100644 --- a/packages/core/src/__tests__/utils/FetchUtil.test.ts +++ b/packages/core/src/__tests__/utils/FetchUtil.test.ts @@ -142,6 +142,47 @@ describe('FetchUtil', () => { expect(arrayBuffer).not.toHaveBeenCalled(); }); + it('should produce correct single-char padding (==) for a one-byte remainder', async () => { + // 'f' -> base64 "Zg==" + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + arrayBuffer: async () => new Uint8Array([0x66]).buffer, + headers: { get: () => 'image/png' } + }) as unknown as typeof fetch; + + const fetchUtil = new FetchUtil({ baseUrl }); + const result = await fetchUtil.fetchImage('/getWalletImage/1'); + + expect(result).toBe('data:image/png;base64,Zg=='); + }); + + it('should produce correct single-char padding (=) for a two-byte remainder', async () => { + // 'fo' -> base64 "Zm8=" + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + arrayBuffer: async () => new Uint8Array([0x66, 0x6f]).buffer, + headers: { get: () => 'image/png' } + }) as unknown as typeof fetch; + + const fetchUtil = new FetchUtil({ baseUrl }); + const result = await fetchUtil.fetchImage('/getWalletImage/1'); + + expect(result).toBe('data:image/png;base64,Zm8='); + }); + + it('should strip content-type parameters when building the data URL', async () => { + global.fetch = jest.fn().mockResolvedValue({ + ok: true, + arrayBuffer: async () => new Uint8Array([0x66, 0x6f, 0x6f, 0x62, 0x61, 0x72]).buffer, + headers: { get: () => 'image/svg+xml; charset=utf-8' } + }) as unknown as typeof fetch; + + const fetchUtil = new FetchUtil({ baseUrl }); + const result = await fetchUtil.fetchImage('/getWalletImage/1'); + + expect(result).toBe('data:image/svg+xml;base64,Zm9vYmFy'); + }); + it('should default the content type to image/png when the header is missing', async () => { global.fetch = jest.fn().mockResolvedValue({ ok: true, diff --git a/packages/core/src/utils/FetchUtil.ts b/packages/core/src/utils/FetchUtil.ts index d932f7658..6057f8274 100644 --- a/packages/core/src/utils/FetchUtil.ts +++ b/packages/core/src/utils/FetchUtil.ts @@ -93,7 +93,11 @@ export class FetchUtil { // (wallet/network images would silently fail to load). Read the bytes as // an ArrayBuffer and base64-encode them into a data URL instead. const arrayBuffer = await response.arrayBuffer(); - const contentType = response.headers.get('content-type') ?? 'image/png'; + // Strip any `; charset=...`/`; quality=...` parameters so the media type + // doesn't leak into the data URL (e.g. `data:image/svg+xml; charset=utf-8; + // base64,...`), which strict parsers may reject. + const rawContentType = response.headers.get('content-type') ?? 'image/png'; + const contentType = rawContentType.split(';')[0]?.trim() || 'image/png'; return `data:${contentType};base64,${FetchUtil._arrayBufferToBase64(arrayBuffer)}`; } catch { From e0f6380bd4ec70f13d3ecfdd108eb365d1fb9095 Mon Sep 17 00:00:00 2001 From: ignaciosantise <25931366+ignaciosantise@users.noreply.github.com> Date: Mon, 13 Jul 2026 17:03:43 -0300 Subject: [PATCH 6/6] fix(core): address review feedback - Correct padding test name ("double-char" for the one-byte remainder `==` case) - Rewrite content-type comment so the example data URL is accurate - Drop the changeset Co-Authored-By: Claude Opus 4.8 --- .changeset/fix-fetch-image-base64-padding.md | 19 ------------------- .../src/__tests__/utils/FetchUtil.test.ts | 2 +- packages/core/src/utils/FetchUtil.ts | 4 ++-- 3 files changed, 3 insertions(+), 22 deletions(-) delete mode 100644 .changeset/fix-fetch-image-base64-padding.md diff --git a/.changeset/fix-fetch-image-base64-padding.md b/.changeset/fix-fetch-image-base64-padding.md deleted file mode 100644 index f104456c0..000000000 --- a/.changeset/fix-fetch-image-base64-padding.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@reown/appkit-react-native': patch -'@reown/appkit-bitcoin-react-native': patch -'@reown/appkit-coinbase-react-native': patch -'@reown/appkit-common-react-native': patch -'@reown/appkit-core-react-native': patch -'@reown/appkit-ethers-react-native': patch -'@reown/appkit-solana-react-native': patch -'@reown/appkit-ui-react-native': patch -'@reown/appkit-wagmi-react-native': patch ---- - -fix(core): strip content-type parameters from fetchImage data URLs - -`FetchUtil.fetchImage` embedded the raw `content-type` header in the data URL, so -a response like `image/svg+xml; charset=utf-8` produced a malformed -`data:image/svg+xml; charset=utf-8;base64,...` that strict parsers may reject. -Strip semicolon-delimited parameters before building the data URL, and add test -coverage for the base64 one-/two-byte padding branches. diff --git a/packages/core/src/__tests__/utils/FetchUtil.test.ts b/packages/core/src/__tests__/utils/FetchUtil.test.ts index 52e0a2e88..bab96f430 100644 --- a/packages/core/src/__tests__/utils/FetchUtil.test.ts +++ b/packages/core/src/__tests__/utils/FetchUtil.test.ts @@ -142,7 +142,7 @@ describe('FetchUtil', () => { expect(arrayBuffer).not.toHaveBeenCalled(); }); - it('should produce correct single-char padding (==) for a one-byte remainder', async () => { + it('should produce correct double-char padding (==) for a one-byte remainder', async () => { // 'f' -> base64 "Zg==" global.fetch = jest.fn().mockResolvedValue({ ok: true, diff --git a/packages/core/src/utils/FetchUtil.ts b/packages/core/src/utils/FetchUtil.ts index 6057f8274..7fd0c3805 100644 --- a/packages/core/src/utils/FetchUtil.ts +++ b/packages/core/src/utils/FetchUtil.ts @@ -94,8 +94,8 @@ export class FetchUtil { // an ArrayBuffer and base64-encode them into a data URL instead. const arrayBuffer = await response.arrayBuffer(); // Strip any `; charset=...`/`; quality=...` parameters so the media type - // doesn't leak into the data URL (e.g. `data:image/svg+xml; charset=utf-8; - // base64,...`), which strict parsers may reject. + // doesn't leak into the data URL as `data:image/svg+xml; charset=utf-8;base64,...`, + // which strict parsers may reject. const rawContentType = response.headers.get('content-type') ?? 'image/png'; const contentType = rawContentType.split(';')[0]?.trim() || 'image/png';