From 246eaed7b383ff497f71ce28dde768605f3a0e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Fri, 31 Oct 2025 15:55:25 +0100 Subject: [PATCH 1/2] fix: iOS production asset loading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes production build issue where iOS bundled assets fail to load. **Problem:** On iOS, `resolveAssetSource()` returns file:// URLs for bundled assets: `file:///path/to/app/assets/rive/rewards_source.riv` The previous implementation tried to extract the filename and load it as a bundle resource, which only worked in debug builds. In production, assets are packaged as actual files in the app bundle, not as named resources accessible via `Bundle.main.path(forResource:)`. **Previous approach (broken in production):** ```typescript // Extract "rewards_source" from file URL const match = assetURI.match(/file:\/\/(.*\/)+(.*)\.riv/); return RiveFileFactory.fromResource(match[2], loadCdn); // ❌ Failed: "Could not find Rive file: rewards_source.riv" ``` **New approach (works in debug and production):** ```typescript // Load directly from file:// URL return RiveFileFactory.fromFileURL(assetURI, loadCdn); // ✅ Reads file directly from disk ``` **Implementation:** - Added `fromFileURL` to `RiveFile.nitro.ts` spec - Implemented `fromFileURL` in iOS (`HybridRiveFileFactory.swift`) - Validates file:// URL scheme - Loads data directly from file path using `Data(contentsOf:)` - Runs on background thread - Implemented `fromFileURL` in Android (`HybridRiveFileFactory.kt`) - Matches iOS validation behavior - Converts URI to path using `java.net.URI` - Loads on `Dispatchers.IO` background thread - Updated `fromSource()` to use `fromFileURL()` for file:// URLs - Regenerated Nitrogen boilerplate 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../nitro/rive/HybridRiveFileFactory.kt | 27 +++++++++++++ ios/HybridRiveFileFactory.swift | 39 +++++++++++++++++++ .../c++/JHybridRiveFileFactorySpec.cpp | 16 ++++++++ .../c++/JHybridRiveFileFactorySpec.hpp | 1 + .../nitro/rive/HybridRiveFileFactorySpec.kt | 4 ++ .../c++/HybridRiveFileFactorySpecSwift.hpp | 8 ++++ .../ios/swift/HybridRiveFileFactorySpec.swift | 1 + .../swift/HybridRiveFileFactorySpec_cxx.swift | 22 +++++++++++ .../shared/c++/HybridRiveFileFactorySpec.cpp | 1 + .../shared/c++/HybridRiveFileFactorySpec.hpp | 1 + src/core/RiveFile.ts | 19 ++++++--- src/specs/RiveFile.nitro.ts | 1 + 12 files changed, 135 insertions(+), 5 deletions(-) diff --git a/android/src/main/java/com/margelo/nitro/rive/HybridRiveFileFactory.kt b/android/src/main/java/com/margelo/nitro/rive/HybridRiveFileFactory.kt index be235f9e..6e390af2 100644 --- a/android/src/main/java/com/margelo/nitro/rive/HybridRiveFileFactory.kt +++ b/android/src/main/java/com/margelo/nitro/rive/HybridRiveFileFactory.kt @@ -9,6 +9,8 @@ import com.margelo.nitro.core.Promise import com.margelo.nitro.NitroModules import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import java.io.File as JavaFile +import java.net.URI import java.net.URL @Keep @@ -33,6 +35,31 @@ class HybridRiveFileFactory : HybridRiveFileFactorySpec() { } } + override fun fromFileURL(fileURL: String, loadCdn: Boolean): Promise { + if (!fileURL.startsWith("file://")) { + throw Error("fromFileURL: URL must be a file URL: $fileURL") + } + + return Promise.async { + try { + val uri = URI(fileURL) + val path = uri.path ?: throw Error("fromFileURL: Invalid URL: $fileURL") + + val riveFile = withContext(Dispatchers.IO) { + val file = JavaFile(path) + val riveData = file.readBytes() + File(riveData) + } + + val hybridRiveFile = HybridRiveFile() + hybridRiveFile.riveFile = riveFile + hybridRiveFile + } catch (e: Exception) { + throw Error("Failed to load Rive file: ${e.message}") + } + } + } + @SuppressLint("DiscouragedApi") override fun fromResource(resource: String, loadCdn: Boolean): Promise { return Promise.async { diff --git a/ios/HybridRiveFileFactory.swift b/ios/HybridRiveFileFactory.swift index 49aaa12e..80e7d512 100644 --- a/ios/HybridRiveFileFactory.swift +++ b/ios/HybridRiveFileFactory.swift @@ -38,6 +38,45 @@ class HybridRiveFileFactory: HybridRiveFileFactorySpec { } } + func fromFileURL(fileURL: String, loadCdn: Bool) throws -> Promise<(any HybridRiveFileSpec)> { + guard let url = URL(string:fileURL) else { + throw RuntimeError.error(withMessage: "fromFileURL: Invalid URL: \(fileURL)") + } + + guard url.isFileURL else { + throw RuntimeError.error(withMessage: "fromFileURL: URL must be a file URL: \(fileURL)") + } + + return Promise.async { + do { + let riveFile = try await withCheckedThrowingContinuation { continuation in + DispatchQueue.global(qos: .userInitiated).async { + do { + let data = try Data(contentsOf: url) + + let riveFile = try RiveFile(data: data, loadCdn: loadCdn) + DispatchQueue.main.async { + continuation.resume(returning: riveFile) + } + } catch { + DispatchQueue.main.async { + continuation.resume(throwing: error) + } + } + } + } + + let hybridRiveFile = HybridRiveFile() + hybridRiveFile.riveFile = riveFile + return hybridRiveFile + } catch let error as NSError { + throw RuntimeError.error(withMessage: "Failed to load Rive file: \(error.localizedDescription)") + } catch { + throw RuntimeError.error(withMessage: "Unknown error occurred while loading Rive file") + } + } + } + func fromResource(resource: String, loadCdn: Bool) throws -> Promise<(any HybridRiveFileSpec)> { guard let _ = Bundle.main.path(forResource: resource, ofType: "riv") else { throw RuntimeError.error(withMessage: "Could not find Rive file: \(resource).riv") diff --git a/nitrogen/generated/android/c++/JHybridRiveFileFactorySpec.cpp b/nitrogen/generated/android/c++/JHybridRiveFileFactorySpec.cpp index 5c8af834..2e214051 100644 --- a/nitrogen/generated/android/c++/JHybridRiveFileFactorySpec.cpp +++ b/nitrogen/generated/android/c++/JHybridRiveFileFactorySpec.cpp @@ -68,6 +68,22 @@ namespace margelo::nitro::rive { return __promise; }(); } + std::shared_ptr>> JHybridRiveFileFactorySpec::fromFileURL(const std::string& fileURL, bool loadCdn) { + static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* fileURL */, jboolean /* loadCdn */)>("fromFileURL"); + auto __result = method(_javaPart, jni::make_jstring(fileURL), loadCdn); + return [&]() { + auto __promise = Promise>::create(); + __result->cthis()->addOnResolvedListener([=](const jni::alias_ref& __boxedResult) { + auto __result = jni::static_ref_cast(__boxedResult); + __promise->resolve(__result->cthis()->shared_cast()); + }); + __result->cthis()->addOnRejectedListener([=](const jni::alias_ref& __throwable) { + jni::JniException __jniError(__throwable); + __promise->reject(std::make_exception_ptr(__jniError)); + }); + return __promise; + }(); + } std::shared_ptr>> JHybridRiveFileFactorySpec::fromResource(const std::string& resource, bool loadCdn) { static const auto method = javaClassStatic()->getMethod(jni::alias_ref /* resource */, jboolean /* loadCdn */)>("fromResource"); auto __result = method(_javaPart, jni::make_jstring(resource), loadCdn); diff --git a/nitrogen/generated/android/c++/JHybridRiveFileFactorySpec.hpp b/nitrogen/generated/android/c++/JHybridRiveFileFactorySpec.hpp index 3cc081f8..63f95a3e 100644 --- a/nitrogen/generated/android/c++/JHybridRiveFileFactorySpec.hpp +++ b/nitrogen/generated/android/c++/JHybridRiveFileFactorySpec.hpp @@ -55,6 +55,7 @@ namespace margelo::nitro::rive { public: // Methods std::shared_ptr>> fromURL(const std::string& url, bool loadCdn) override; + std::shared_ptr>> fromFileURL(const std::string& fileURL, bool loadCdn) override; std::shared_ptr>> fromResource(const std::string& resource, bool loadCdn) override; std::shared_ptr>> fromBytes(const std::shared_ptr& bytes, bool loadCdn) override; diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveFileFactorySpec.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveFileFactorySpec.kt index 9d08b46a..a8c24d52 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveFileFactorySpec.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveFileFactorySpec.kt @@ -51,6 +51,10 @@ abstract class HybridRiveFileFactorySpec: HybridObject() { @Keep abstract fun fromURL(url: String, loadCdn: Boolean): Promise + @DoNotStrip + @Keep + abstract fun fromFileURL(fileURL: String, loadCdn: Boolean): Promise + @DoNotStrip @Keep abstract fun fromResource(resource: String, loadCdn: Boolean): Promise diff --git a/nitrogen/generated/ios/c++/HybridRiveFileFactorySpecSwift.hpp b/nitrogen/generated/ios/c++/HybridRiveFileFactorySpecSwift.hpp index f13df8a8..ea715f39 100644 --- a/nitrogen/generated/ios/c++/HybridRiveFileFactorySpecSwift.hpp +++ b/nitrogen/generated/ios/c++/HybridRiveFileFactorySpecSwift.hpp @@ -76,6 +76,14 @@ namespace margelo::nitro::rive { auto __value = std::move(__result.value()); return __value; } + inline std::shared_ptr>> fromFileURL(const std::string& fileURL, bool loadCdn) override { + auto __result = _swiftPart.fromFileURL(fileURL, std::forward(loadCdn)); + if (__result.hasError()) [[unlikely]] { + std::rethrow_exception(__result.error()); + } + auto __value = std::move(__result.value()); + return __value; + } inline std::shared_ptr>> fromResource(const std::string& resource, bool loadCdn) override { auto __result = _swiftPart.fromResource(resource, std::forward(loadCdn)); if (__result.hasError()) [[unlikely]] { diff --git a/nitrogen/generated/ios/swift/HybridRiveFileFactorySpec.swift b/nitrogen/generated/ios/swift/HybridRiveFileFactorySpec.swift index 932d5a4b..c944d7c8 100644 --- a/nitrogen/generated/ios/swift/HybridRiveFileFactorySpec.swift +++ b/nitrogen/generated/ios/swift/HybridRiveFileFactorySpec.swift @@ -16,6 +16,7 @@ public protocol HybridRiveFileFactorySpec_protocol: HybridObject { // Methods func fromURL(url: String, loadCdn: Bool) throws -> Promise<(any HybridRiveFileSpec)> + func fromFileURL(fileURL: String, loadCdn: Bool) throws -> Promise<(any HybridRiveFileSpec)> func fromResource(resource: String, loadCdn: Bool) throws -> Promise<(any HybridRiveFileSpec)> func fromBytes(bytes: ArrayBuffer, loadCdn: Bool) throws -> Promise<(any HybridRiveFileSpec)> } diff --git a/nitrogen/generated/ios/swift/HybridRiveFileFactorySpec_cxx.swift b/nitrogen/generated/ios/swift/HybridRiveFileFactorySpec_cxx.swift index c0a8ef4a..f08a5d25 100644 --- a/nitrogen/generated/ios/swift/HybridRiveFileFactorySpec_cxx.swift +++ b/nitrogen/generated/ios/swift/HybridRiveFileFactorySpec_cxx.swift @@ -140,6 +140,28 @@ open class HybridRiveFileFactorySpec_cxx { } } + @inline(__always) + public final func fromFileURL(fileURL: std.string, loadCdn: Bool) -> bridge.Result_std__shared_ptr_Promise_std__shared_ptr_HybridRiveFileSpec____ { + do { + let __result = try self.__implementation.fromFileURL(fileURL: String(fileURL), loadCdn: loadCdn) + let __resultCpp = { () -> bridge.std__shared_ptr_Promise_std__shared_ptr_HybridRiveFileSpec___ in + let __promise = bridge.create_std__shared_ptr_Promise_std__shared_ptr_HybridRiveFileSpec___() + let __promiseHolder = bridge.wrap_std__shared_ptr_Promise_std__shared_ptr_HybridRiveFileSpec___(__promise) + __result + .then({ __result in __promiseHolder.resolve({ () -> bridge.std__shared_ptr_HybridRiveFileSpec_ in + let __cxxWrapped = __result.getCxxWrapper() + return __cxxWrapped.getCxxPart() + }()) }) + .catch({ __error in __promiseHolder.reject(__error.toCpp()) }) + return __promise + }() + return bridge.create_Result_std__shared_ptr_Promise_std__shared_ptr_HybridRiveFileSpec____(__resultCpp) + } catch (let __error) { + let __exceptionPtr = __error.toCpp() + return bridge.create_Result_std__shared_ptr_Promise_std__shared_ptr_HybridRiveFileSpec____(__exceptionPtr) + } + } + @inline(__always) public final func fromResource(resource: std.string, loadCdn: Bool) -> bridge.Result_std__shared_ptr_Promise_std__shared_ptr_HybridRiveFileSpec____ { do { diff --git a/nitrogen/generated/shared/c++/HybridRiveFileFactorySpec.cpp b/nitrogen/generated/shared/c++/HybridRiveFileFactorySpec.cpp index c4013f28..34545b01 100644 --- a/nitrogen/generated/shared/c++/HybridRiveFileFactorySpec.cpp +++ b/nitrogen/generated/shared/c++/HybridRiveFileFactorySpec.cpp @@ -15,6 +15,7 @@ namespace margelo::nitro::rive { // load custom methods/properties registerHybrids(this, [](Prototype& prototype) { prototype.registerHybridMethod("fromURL", &HybridRiveFileFactorySpec::fromURL); + prototype.registerHybridMethod("fromFileURL", &HybridRiveFileFactorySpec::fromFileURL); prototype.registerHybridMethod("fromResource", &HybridRiveFileFactorySpec::fromResource); prototype.registerHybridMethod("fromBytes", &HybridRiveFileFactorySpec::fromBytes); }); diff --git a/nitrogen/generated/shared/c++/HybridRiveFileFactorySpec.hpp b/nitrogen/generated/shared/c++/HybridRiveFileFactorySpec.hpp index 8a3a2842..b84d85d7 100644 --- a/nitrogen/generated/shared/c++/HybridRiveFileFactorySpec.hpp +++ b/nitrogen/generated/shared/c++/HybridRiveFileFactorySpec.hpp @@ -54,6 +54,7 @@ namespace margelo::nitro::rive { public: // Methods virtual std::shared_ptr>> fromURL(const std::string& url, bool loadCdn) = 0; + virtual std::shared_ptr>> fromFileURL(const std::string& fileURL, bool loadCdn) = 0; virtual std::shared_ptr>> fromResource(const std::string& resource, bool loadCdn) = 0; virtual std::shared_ptr>> fromBytes(const std::shared_ptr& bytes, bool loadCdn) = 0; diff --git a/src/core/RiveFile.ts b/src/core/RiveFile.ts index d5be8abe..d8eba328 100644 --- a/src/core/RiveFile.ts +++ b/src/core/RiveFile.ts @@ -29,6 +29,19 @@ export namespace RiveFileFactory { return RiveFileInternal.fromURL(url, loadCdn); } + /** + * Creates a RiveFile instance from a local file path URL. + * @param pathURL - The local file path of the Rive animation file + * @param loadCdn - Whether to load from CDN (default: true) + * @returns Promise that resolves to a RiveFile instance + */ + export async function fromFileURL( + fileURL: string, + loadCdn: boolean = true + ): Promise { + return RiveFileInternal.fromFileURL(fileURL, loadCdn); + } + /** * Creates a RiveFile instance from a local resource. * @param resource - The name of the local resource @@ -100,11 +113,7 @@ export namespace RiveFileFactory { // handle iOS bundled asset if (assetURI.match(/file:\/\//)) { - const match = assetURI.match(/file:\/\/(.*\/)+(.*)\.riv/); - if (!match) { - throw new Error(`Invalid iOS asset path format: ${assetURI}`); - } - return RiveFileFactory.fromResource(match[2], loadCdn); + return RiveFileFactory.fromFileURL(assetURI, loadCdn); } // handle Android bundled asset or resource name uri diff --git a/src/specs/RiveFile.nitro.ts b/src/specs/RiveFile.nitro.ts index 0337a8c9..9db60746 100644 --- a/src/specs/RiveFile.nitro.ts +++ b/src/specs/RiveFile.nitro.ts @@ -22,6 +22,7 @@ export interface RiveFile export interface RiveFileFactory extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { fromURL(url: string, loadCdn: boolean): Promise; + fromFileURL(fileURL: string, loadCdn: boolean): Promise; fromResource(resource: string, loadCdn: boolean): Promise; fromBytes(bytes: ArrayBuffer, loadCdn: boolean): Promise; } From 2d99454e77d32fabb87ea202826fce5467c2d983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Mon, 3 Nov 2025 12:57:38 +0100 Subject: [PATCH 2/2] Update src/core/RiveFile.ts Co-authored-by: Gordon --- src/core/RiveFile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/RiveFile.ts b/src/core/RiveFile.ts index d8eba328..cdebe93a 100644 --- a/src/core/RiveFile.ts +++ b/src/core/RiveFile.ts @@ -31,7 +31,7 @@ export namespace RiveFileFactory { /** * Creates a RiveFile instance from a local file path URL. - * @param pathURL - The local file path of the Rive animation file + * @param pathURL - The local file path of the Rive graphic file * @param loadCdn - Whether to load from CDN (default: true) * @returns Promise that resolves to a RiveFile instance */