From 7d90761de17bba293eb8d51e322e0269df298959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 11:49:21 +0200 Subject: [PATCH 01/56] rewrite existing feature scope to flutter webview, remove native sdk dependencies --- CHANGELOG.md | 15 + README.md | 52 +++- android/build.gradle | 7 +- .../ramp/ramp_flutter/RampFlutterPlugin.kt | 183 +----------- example/README.md | 15 +- example/android/app/build.gradle | 6 +- .../android/app/src/main/AndroidManifest.xml | 2 + example/android/build.gradle | 1 - example/ios/Podfile | 4 +- example/ios/Runner.xcodeproj/project.pbxproj | 6 +- example/ios/Runner/Info.plist | 4 +- example/lib/main.dart | 77 ++--- example/pubspec.lock | 137 ++++++++- ios/Classes/RampFlutterPlugin.swift | 175 +----------- ios/ramp_flutter.podspec | 12 +- lib/configuration.dart | 78 +++-- lib/internal/ramp_webview_controller.dart | 193 +++++++++++++ lib/internal/ramp_webview_page.dart | 16 ++ lib/offramp_sale.dart | 5 +- lib/onramp_purchase.dart | 15 +- lib/ramp_flutter.dart | 95 +++--- lib/send_crypto_payload.dart | 20 +- pubspec.lock | 270 +++++++++++++++++- pubspec.yaml | 11 +- test/ramp_webview_test.dart | 154 ++++++++++ 25 files changed, 971 insertions(+), 582 deletions(-) create mode 100644 lib/internal/ramp_webview_controller.dart create mode 100644 lib/internal/ramp_webview_page.dart create mode 100644 test/ramp_webview_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index c09dfb1..2e0d89c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 5.0.0 + +* Rewrite the SDK to load the Ramp widget in a Flutter WebView instead of the + native iOS/Android Ramp SDKs. +* Breaking: `showRamp` now requires a `BuildContext` and pushes a fullscreen + route (`showRamp(context, configuration)`). +* Add `Configuration.offrampAsset` and build the widget URL in Dart + (`Configuration.buildWidgetUrl`), including `sdkType` / `sdkVersion` / + `variant`. +* Default base URL is now `https://app.rampnetwork.com`. +* Raise minimum platforms to Android 7.0 (API 24) and iOS 13. +* Fix numeric event fields crashing on whole-number JSON values; + `OnrampPurchase.fiatValue` is now `double?`. +* Remove unused `SendCryptoPayload.toMap` and `SendCryptoAssetInfo.toMap`. + ## 4.0.1 * Updated package documentation diff --git a/README.md b/README.md index 367853c..a9d3a00 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,55 @@ # Ramp Network Flutter -Official Flutter wrapper for Ramp Network +Official Flutter SDK for Ramp Network. Loads the Ramp widget in a Flutter +WebView on iOS and Android. ## Getting Started -For installation & usage follow [Ramp Network Flutter documentation](https://docs.ramp.network/mobile/flutter-sdk/) +Add the dependency: + +```yaml +dependencies: + ramp_flutter: ^5.0.0 +``` + +### Host setup + +- **Android:** `minSdkVersion` 24+. Declare `android.permission.CAMERA` if you + need KYC camera capture. +- **iOS:** deployment target 13+. Add `NSCameraUsageDescription` (and photo + library usage if you rely on document upload). +- Native Ramp iOS/Android SDKs are **not** required (no CocoaPods `Ramp` pod, + no JitPack `ramp-sdk-android`). + +### Usage + +```dart +final ramp = RampFlutter(); +ramp.onOnrampPurchaseCreated = (purchase, token, apiUrl) {}; +ramp.onOfframpSaleCreated = (sale, token, apiUrl) {}; +ramp.onSendCryptoRequested = (payload) { + // Send crypto, then: + ramp.sendCrypto(txHash); +}; +ramp.onRampClosed = () {}; + +final configuration = Configuration() + ..hostApiKey = 'YOUR_API_KEY' + ..hostAppName = 'My App' + ..hostLogoUrl = 'https://example.com/logo.png' + ..enabledFlows = ['ONRAMP', 'OFFRAMP']; + +await ramp.showRamp(context, configuration); +``` + +`showRamp` requires a `BuildContext` with a `Navigator` and presents a +fullscreen WebView route. + +For more configuration parameters see +[Ramp Network Flutter documentation](https://docs.ramp.network/mobile/flutter-sdk/). + +### Notes + +- Android document file upload from the WebView is not wired in this SDK yet. +- Server-signed widget URLs are not supported in this release; use + `Configuration` fields so the SDK can build the widget URL. diff --git a/android/build.gradle b/android/build.gradle index 61d196e..bb5d422 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -18,7 +18,6 @@ rootProject.allprojects { repositories { google() mavenCentral() - maven { url 'https://jitpack.io' } } } @@ -46,10 +45,6 @@ android { } defaultConfig { - minSdkVersion 21 - } - - dependencies { - implementation 'com.github.RampNetwork:ramp-sdk-android:4.0.+' + minSdkVersion 24 } } diff --git a/android/src/main/kotlin/network/ramp/ramp_flutter/RampFlutterPlugin.kt b/android/src/main/kotlin/network/ramp/ramp_flutter/RampFlutterPlugin.kt index 1138f52..a4e72f9 100644 --- a/android/src/main/kotlin/network/ramp/ramp_flutter/RampFlutterPlugin.kt +++ b/android/src/main/kotlin/network/ramp/ramp_flutter/RampFlutterPlugin.kt @@ -1,185 +1,10 @@ package network.ramp.ramp_flutter -import android.app.Activity -import android.content.Context import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import network.ramp.sdk.events.model.Asset -import network.ramp.sdk.events.model.OfframpSale -import network.ramp.sdk.events.model.Purchase -import network.ramp.sdk.facade.Config -import network.ramp.sdk.facade.Flow -import network.ramp.sdk.facade.RampCallback -import network.ramp.sdk.facade.RampSDK -/** RampFlutterPlugin */ -class RampFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { - private lateinit var channel: MethodChannel - private lateinit var context: Context - private lateinit var activity: Activity - private lateinit var rampSdk: RampSDK - private lateinit var callback: RampCallback +/** No-op plugin registration; widget flow is implemented in Dart via WebView. */ +class RampFlutterPlugin : FlutterPlugin { + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {} - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "ramp_flutter") - channel.setMethodCallHandler(this) - context = flutterPluginBinding.applicationContext - - rampSdk = RampSDK() - callback = object : RampCallback { - override fun onPurchaseCreated(purchase: Purchase, purchaseViewToken: String, apiUrl: String) { - val purchaseMap = serializePurchase(purchase) - val arguments = listOf(purchaseMap, purchaseViewToken, apiUrl) - channel.invokeMethod("onOnrampPurchaseCreated", arguments) - } - - override fun offrampSendCrypto(assetInfo: Asset, amount: String, address: String) { - val assetMap = serializeAsset(assetInfo) - val arguments = listOf(mapOf("address" to address, "amount" to amount, "assetInfo" to assetMap)) - channel.invokeMethod("onSendCryptoRequested", arguments) - } - - override fun onOfframpSaleCreated(sale: OfframpSale, saleViewToken: String, apiUrl: String) { - val saleMap = serializeOfframpSale(sale) - val arguments = listOf(saleMap, saleViewToken, apiUrl) - channel.invokeMethod("onOfframpSaleCreated", arguments) - } - - override fun onWidgetClose() { - channel.invokeMethod("onRampClosed", null) - } - } - } - - override fun onMethodCall(call: MethodCall, result: Result) { - when (call.method) { - "showRamp" -> { - showRamp(call.arguments) - result.success(null) - } - "sendCrypto" -> { - sendCrypto(call.arguments) - result.success(null) - } - else -> { - result.notImplemented() - } - } - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } - - override fun onDetachedFromActivity() { - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - } - - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activity = binding.activity - } - - override fun onDetachedFromActivityForConfigChanges() { - } - - private fun showRamp(arguments: Any) { - val config = makeConfiguration(arguments) ?: return - rampSdk.startTransaction(activity, config, callback) - } - - private fun sendCrypto(arguments: Any) { - val txHash = arguments as? String - rampSdk.onOfframpCryptoSent(txHash) - } -} - -private fun makeConfiguration(arguments: Any): Config? { - val map = arguments as? Map<*, *> ?: return null - val hostAppName = map["hostAppName"] as? String ?: "Ramp Network Integration" - val hostLogoUrl = map["hostLogoUrl"] as? String ?: "https://ramp.network/assets/images/Logo.svg" - val url = map["url"] as? String ?: "https://app.ramp.network" - val config = Config(hostAppName, hostLogoUrl, url) - - config.defaultAsset = map["defaultAsset"] as? String ?: "" - val rawDefaultFlow = map["defaultFlow"] as? String ?: "" - unwrapFlow(rawDefaultFlow)?.let { flow -> config.defaultFlow = flow } - - val rawEnabledFlows = map["enabledFlows"] as? List ?: listOf() - val enabledFlows = rawEnabledFlows.mapNotNull { unwrapFlow(it) } - if (enabledFlows.isNotEmpty()) { - config.enabledFlows = HashSet(enabledFlows) - } - - config.fiatCurrency = map["fiatCurrency"] as? String ?: "" - config.fiatValue = map["fiatValue"] as? String ?: "" - config.hostApiKey = map["hostApiKey"] as? String ?: "" - config.selectedCountryCode = map["selectedCountryCode"] as? String ?: "" - config.swapAmount = map["swapAmount"] as? String ?: "" - config.swapAsset = map["swapAsset"] as? String ?: "" - config.userAddress = map["userAddress"] as? String ?: "" - config.userEmailAddress = map["userEmailAddress"] as? String ?: "" - config.useSendCryptoCallback = map["useSendCryptoCallback"] as? Boolean ?: false - config.webhookStatusUrl = map["webhookStatusUrl"] as? String ?: "" - - return config -} - -private fun unwrapFlow(rawFlow: String): Flow? { - return when(rawFlow) { - "ONRAMP" -> Flow.ONRAMP - "OFFRAMP" -> Flow.OFFRAMP - else -> null - } -} - -private fun serializePurchase(purchase: Purchase): Map { - return mapOf( - "id" to purchase.id, - "endTime" to purchase.endTime, - "asset" to serializeAsset(purchase.asset), - "receiverAddress" to purchase.receiverAddress, - "cryptoAmount" to purchase.cryptoAmount, - "fiatCurrency" to purchase.fiatCurrency, - "fiatValue" to purchase.fiatValue, - "assetExchangeRate" to purchase.assetExchangeRate, - "baseRampFee" to purchase.baseRampFee, - "networkFee" to purchase.networkFee, - "appliedFee" to purchase.appliedFee, - "paymentMethodType" to purchase.paymentMethodType, - "createdAt" to purchase.createdAt, - "updatedAt" to purchase.updatedAt, - "status" to purchase.status, - ) -} - -private fun serializeAsset(asset: Asset): Map { - return mapOf( - "address" to asset.address, - "decimals" to asset.decimals, - "name" to asset.name, - "symbol" to asset.symbol, - "type" to asset.type, - ) -} - -private fun serializeOfframpSale(offrampSale: OfframpSale): Map { - return mapOf( - "id" to offrampSale.id, - "createdAt" to offrampSale.createdAt, - "crypto" to mapOf( - "amount" to offrampSale.crypto.amount, - "assetInfo" to serializeAsset(offrampSale.crypto.assetInfo), - ), - "fiat" to mapOf( - "amount" to offrampSale.fiat.amount, - "currencySymbol" to offrampSale.fiat.currencySymbol, - ) - ) + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {} } diff --git a/example/README.md b/example/README.md index 1f062de..08f0827 100644 --- a/example/README.md +++ b/example/README.md @@ -1,16 +1,5 @@ # ramp_flutter_example -Demonstrates how to use the ramp_flutter plugin. +Demonstrates the Ramp Network Flutter WebView SDK (`ramp_flutter` 5.0.0). -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +Use **Show Ramp** to present the widget via `ramp.showRamp(context, configuration)`. diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 9f25e16..14b6fdf 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -42,7 +42,7 @@ android { defaultConfig { applicationId "network.ramp.ramp_flutter_example" - minSdkVersion 21 + minSdkVersion 24 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -57,8 +57,4 @@ android { flutter { source '../..' -} - -dependencies { - implementation 'com.github.RampNetwork:ramp-sdk-android:3.+' } \ No newline at end of file diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 24b215d..a7d958a 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,6 @@ + + 'https://github.com/RampNetwork/ramp-sdk-ios', :tag => '4.0.2' flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 7593176..349ef05 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -344,7 +344,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -423,7 +423,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -473,7 +473,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 88b7085..f9136ac 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -42,9 +42,9 @@ LSRequiresIPhoneOS NSCameraUsageDescription - X + Camera access is required for identity verification in Ramp Network. NSPhotoLibraryUsageDescription - X + Photo library access is used to upload verification documents in Ramp Network. UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName diff --git a/example/lib/main.dart b/example/lib/main.dart index 37389b4..ac4f0d4 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -44,19 +44,19 @@ class _RampFlutterAppState extends State { final List _predefinedEnvironments = [ "https://app.dev.ramp-network.org", "https://ri-widget-staging.firebaseapp.com", - "https://app.ramp.network", + "https://app.rampnetwork.com", ]; int _selectedEnvironment = 1; - bool _useFullCustomUrl = false; - String _fullCustomUrl = ""; @override void initState() { _configuration.hostAppName = "Ramp Network Flutter"; + _configuration.hostLogoUrl = "https://ramp.network/assets/images/Logo.svg"; _configuration.url = _predefinedEnvironments[_selectedEnvironment]; _configuration.enabledFlows = ["ONRAMP", "OFFRAMP"]; _configuration.useSendCryptoCallback = true; + _configuration.deepLinkScheme = "rampflutterdemo"; ramp.onOnrampPurchaseCreated = onOnrampPurchaseCreated; ramp.onSendCryptoRequested = onSendCryptoRequested; @@ -100,14 +100,16 @@ class _RampFlutterAppState extends State { @override Widget build(BuildContext context) { return PlatformApp( - home: PlatformScaffold( - appBar: PlatformAppBar( - title: const Text('Ramp Network Flutter'), - ), - body: Padding( - padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), - child: ListView( - children: _formFields(context), + home: Builder( + builder: (context) => PlatformScaffold( + appBar: PlatformAppBar( + title: const Text('Ramp Network Flutter'), + ), + body: Padding( + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: ListView( + children: _formFields(context), + ), ), ), ), @@ -115,30 +117,11 @@ class _RampFlutterAppState extends State { } List _formFields(BuildContext context) { - List widgets = []; - widgets.add(_useFullCustomUrlToggle()); - if (_useFullCustomUrl) { - widgets.addAll(_customUrlForm()); - } else { - widgets.addAll(_configurationForm()); - } - widgets.add(_showRampButton()); - widgets.add(_appInfo()); - return widgets; + return [..._configurationForm(), _showRampButton(context), _appInfo()]; } Widget _appInfo() { - return PlatformText("App version: Flutter"); - } - - List _customUrlForm() { - return [ - _textField( - "Full custom URL", - (text) => _fullCustomUrl = text, - _fullCustomUrl, - ) - ]; + return PlatformText("App version: Flutter WebView"); } List _configurationForm() { @@ -174,6 +157,11 @@ class _RampFlutterAppState extends State { (text) => _configuration.defaultAsset = text, _configuration.defaultAsset, ), + _textField( + "Offramp asset", + (text) => _configuration.offrampAsset = text, + _configuration.offrampAsset, + ), _textField( "User address", (text) => _configuration.userAddress = text, @@ -240,34 +228,13 @@ class _RampFlutterAppState extends State { ]); } - Widget _showRampButton() { + Widget _showRampButton(BuildContext context) { return PlatformTextButton( - onPressed: () { - if (_useFullCustomUrl) { - Configuration c = Configuration(); - c.url = _fullCustomUrl; - ramp.showRamp(c); - } else { - ramp.showRamp(_configuration); - } - }, + onPressed: () => ramp.showRamp(context, _configuration), child: PlatformText("Show Ramp"), ); } - Widget _useFullCustomUrlToggle() { - return Row( - children: [ - PlatformText("Use full custom URL"), - const Spacer(), - PlatformSwitch( - value: _useFullCustomUrl, - onChanged: (value) => setState(() => _useFullCustomUrl = value), - ) - ], - ); - } - Row _segmentedControl( String title, List options, void Function(int) itemSelected) { List segments = options.asMap().entries.map((entry) { diff --git a/example/pubspec.lock b/example/pubspec.lock index 9707c6d..275c9f4 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" clock: dependency: transitive description: @@ -29,10 +29,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" cupertino_icons: dependency: "direct main" description: @@ -102,6 +102,11 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" lints: dependency: transitive description: @@ -114,18 +119,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.18.0" path: dependency: transitive description: @@ -156,12 +161,12 @@ packages: path: ".." relative: true source: path - version: "3.0.0" + version: "5.0.0" sky_engine: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" timezone: dependency: transitive description: @@ -170,14 +175,118 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.4" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 + url: "https://pub.dev" + source: hosted + version: "6.3.32" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736 + url: "https://pub.dev" + source: hosted + version: "4.9.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558" + url: "https://pub.dev" + source: hosted + version: "3.16.9" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" + url: "https://pub.dev" + source: hosted + version: "2.15.1" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: c879dd64b87c452aa84381b244d5469da57ba7e8cca6884c7b1e0d406372c12d url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "3.26.0" xdg_directories: dependency: transitive description: @@ -195,5 +304,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.3.0-279.1.beta <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/ios/Classes/RampFlutterPlugin.swift b/ios/Classes/RampFlutterPlugin.swift index f503dbd..429d37a 100644 --- a/ios/Classes/RampFlutterPlugin.swift +++ b/ios/Classes/RampFlutterPlugin.swift @@ -1,176 +1,7 @@ import Flutter import UIKit -import Ramp -public class RampFlutterPlugin: NSObject { - private let channel: FlutterMethodChannel - private var sendCryptoResponseHandler: ((SendCryptoResultPayload) -> Void)? - - init(channel: FlutterMethodChannel) { - self.channel = channel - } - - private func showRamp(arguments: Any?) throws { - guard let delegate = UIApplication.shared.delegate, - let window = delegate.window, - let flutterViewController = window?.rootViewController as? FlutterViewController - else { - throw RampFlutterError.flutterViewControllerUnavailable - } - - guard let configurationArguments = arguments as? [String: Any], - let configuration = try? Configuration.from(configurationArguments) - else { - throw RampFlutterError.unableToDecodeConfiguration - } - - let rampViewController = try RampViewController(configuration: configuration) - rampViewController.delegate = self - flutterViewController.present(rampViewController, animated: true) - } - - private func sendCrypto(arguments: Any?) throws { - guard let transactionHash = arguments as? String - else { - throw RampFlutterError.unableToDecodeTransactionHash - } - sendCryptoResponseHandler?(SendCryptoResultPayload(txHash: transactionHash)) - } -} - -extension RampFlutterPlugin: FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "ramp_flutter", binaryMessenger: registrar.messenger()) - let instance = RampFlutterPlugin(channel: channel) - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "showRamp": - do { - try showRamp(arguments: call.arguments) - result(nil) - } - catch { - result(error.flutterError) - } - - case "sendCrypto": - do { - try sendCrypto(arguments: call.arguments) - result(nil) - } - catch { - result(error.flutterError) - } - - default: - let error = RampFlutterError.unknownCallMethod - result(error.flutterError) - } - } -} - -extension RampFlutterPlugin: RampDelegate { - public func ramp(_ rampViewController: RampViewController, - didCreateOnrampPurchase purchase: OnrampPurchase, - _ purchaseViewToken: String, - _ apiUrl: URL) { - guard let purchase = try? purchase.toDictionary() else { return } - let apiUrl = apiUrl.absoluteString - channel.invokeMethod("onOnrampPurchaseCreated", - arguments: [purchase, purchaseViewToken, apiUrl]) - - } - - public func ramp(_ rampViewController: RampViewController, - didRequestSendCrypto payload: SendCryptoPayload, - responseHandler: @escaping (SendCryptoResultPayload) -> Void) { - guard let payload = try? payload.toDictionary() else { return } - self.sendCryptoResponseHandler = responseHandler - channel.invokeMethod("onSendCryptoRequested", - arguments: [payload]) - } - - public func ramp(_ rampViewController: RampViewController, - didCreateOfframpSale sale: OfframpSale, - _ saleViewToken: String, - _ apiUrl: URL) { - guard let sale = try? sale.toDictionary() else { return } - let url = apiUrl.absoluteString - channel.invokeMethod("onOfframpSaleCreated", - arguments: [sale, saleViewToken, url]) - } - - public func rampDidClose(_ rampViewController: RampViewController) { - channel.invokeMethod("onRampClosed", arguments: nil) - } -} - -private extension Configuration { - static func from(_ dictionary: [String: Any]) throws -> Configuration { - let data = try JSONSerialization.data(withJSONObject: dictionary) - let configuration = try JSONDecoder().decode(Configuration.self, from: data) - return configuration - } -} - -private extension OnrampPurchase { - func toDictionary() throws -> Any { - let data = try JSONEncoder().encode(self) - let dictionary = try JSONSerialization.jsonObject(with: data) - return dictionary - } -} - -private extension OfframpSale { - func toDictionary() throws -> Any { - let data = try JSONEncoder().encode(self) - let dictionary = try JSONSerialization.jsonObject(with: data) - return dictionary - } -} - -private extension SendCryptoPayload { - func toDictionary() throws -> Any { - let data = try JSONEncoder().encode(self) - let dictionary = try JSONSerialization.jsonObject(with: data) - return dictionary - } -} - -private enum RampFlutterError: Error { - case flutterViewControllerUnavailable - case unableToDecodeConfiguration - case unableToDecodeTransactionHash - case unknownCallMethod -} - -private extension Error { - var flutterError: FlutterError { - switch self as? RampFlutterError { - case .flutterViewControllerUnavailable: - return FlutterError(code: "flutterViewControllerUnavailable", - message: "FlutterViewController unavailable", - details: nil) - case .unableToDecodeConfiguration: - return FlutterError(code: "unableToDecodeConfiguration", - message: "Unable to decode Configuration", - details: nil) - case .unableToDecodeTransactionHash: - return FlutterError(code: "unableToDecodeTransactionHash", - message: "Unable to decode transaction hash", - details: nil) - case .unknownCallMethod: - return FlutterError(code: "unknownCallMethod", - message: "Unknown call method", - details: nil) - case .none: - let nsError = self as NSError - return FlutterError(code: String(nsError.code), - message: nsError.description, - details: nsError.userInfo) - } - } +/// No-op plugin registration; widget flow is implemented in Dart via WebView. +public class RampFlutterPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) {} } diff --git a/ios/ramp_flutter.podspec b/ios/ramp_flutter.podspec index 55a3e4a..801d4e6 100644 --- a/ios/ramp_flutter.podspec +++ b/ios/ramp_flutter.podspec @@ -1,11 +1,10 @@ Pod::Spec.new do |s| s.name = 'ramp_flutter' - s.version = '1.0.1' - s.summary = 'Ramp Network iOS wrapper for Flutter.' + s.version = '5.0.0' + s.summary = 'Ramp Network Flutter SDK.' s.description = <<-DESC - Ramp Network for Flutter is a simple wrapper for native iOS and Android Ramp Network SDKs. - Unified API lets you write code once and use on any of the platforms. - Ramp Network Flutter SDK supports iOS and Android platforms. + Ramp Network for Flutter loads the Ramp widget in a Flutter WebView with a + unified Dart API for iOS and Android. DESC s.homepage = 'https://docs.ramp.network/mobile/flutter-sdk/' s.license = 'MIT' @@ -13,8 +12,7 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.dependency 'Ramp' - s.platform = :ios, '11.0' + s.platform = :ios, '13.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/lib/configuration.dart b/lib/configuration.dart index 47836ac..9ba6ff7 100644 --- a/lib/configuration.dart +++ b/lib/configuration.dart @@ -1,8 +1,12 @@ class Configuration { - // main URL + static const String defaultUrl = 'https://app.rampnetwork.com'; + static const String sdkType = 'Flutter'; + static const String sdkVersion = '5.0.0'; + static const String mobileSdkVariant = 'sdk-mobile'; + + /// Base widget URL (scheme + host + optional path). Query is built from fields below. String? url; - // query params String? containerNode; String? deepLinkScheme; String? defaultAsset; @@ -14,6 +18,7 @@ class Configuration { String? hostApiKey; String? hostAppName; String? hostLogoUrl; + String? offrampAsset; String? offrampWebhookV3Url; String? selectedCountryCode; String? swapAmount; @@ -21,32 +26,55 @@ class Configuration { String? userAddress; String? userEmailAddress; bool? useSendCryptoCallback; + /// Ignored when building the URL; the SDK always sends [mobileSdkVariant]. String? variant; String? webhookStatusUrl; - dynamic toMap() { - return { - 'url': url ?? 'https://app.ramp.network', // main URL - 'containerNode': containerNode, - 'deepLinkScheme': deepLinkScheme, - 'defaultAsset': defaultAsset, - 'defaultFlow': defaultFlow, - 'enabledFlows': enabledFlows, - 'fiatCurrency': fiatCurrency, - 'fiatValue': fiatValue, - 'finalUrl': finalUrl, - 'hostApiKey': hostApiKey, - 'hostAppName': hostAppName, - 'hostLogoUrl': hostLogoUrl, - 'offrampWebhookV3Url': offrampWebhookV3Url, - 'selectedCountryCode': selectedCountryCode, - 'swapAmount': swapAmount, - 'swapAsset': swapAsset, - 'userAddress': userAddress, - 'userEmailAddress': userEmailAddress, - 'useSendCryptoCallback': useSendCryptoCallback, - 'variant': variant, - 'webhookStatusUrl': webhookStatusUrl, + /// Builds the widget URL from [url] and configuration query parameters. + Uri buildWidgetUrl() { + final base = Uri.parse( + (url != null && url!.trim().isNotEmpty) ? url!.trim() : defaultUrl, + ); + + final queryParameters = { + ...base.queryParameters, + if (_nonEmpty(containerNode)) 'containerNode': containerNode!, + if (_nonEmpty(deepLinkScheme)) 'deepLinkScheme': deepLinkScheme!, + if (_nonEmpty(defaultAsset)) 'defaultAsset': defaultAsset!, + if (_nonEmpty(defaultFlow)) 'defaultFlow': defaultFlow!, + if (enabledFlows != null && enabledFlows!.isNotEmpty) + 'enabledFlows': enabledFlows!.join(','), + if (_nonEmpty(fiatCurrency)) 'fiatCurrency': fiatCurrency!, + if (_nonEmpty(fiatValue)) 'fiatValue': fiatValue!, + if (_nonEmpty(finalUrl)) 'finalUrl': finalUrl!, + if (_nonEmpty(hostApiKey)) 'hostApiKey': hostApiKey!, + if (_nonEmpty(hostAppName)) 'hostAppName': hostAppName!, + if (_nonEmpty(hostLogoUrl)) 'hostLogoUrl': hostLogoUrl!, + if (_nonEmpty(offrampAsset)) 'offrampAsset': offrampAsset!, + if (_nonEmpty(offrampWebhookV3Url)) + 'offrampWebhookV3Url': offrampWebhookV3Url!, + if (_nonEmpty(selectedCountryCode)) + 'selectedCountryCode': selectedCountryCode!, + if (_nonEmpty(swapAmount)) 'swapAmount': swapAmount!, + if (_nonEmpty(swapAsset)) 'swapAsset': swapAsset!, + if (_nonEmpty(userAddress)) 'userAddress': userAddress!, + if (_nonEmpty(userEmailAddress)) 'userEmailAddress': userEmailAddress!, + if (_nonEmpty(webhookStatusUrl)) 'webhookStatusUrl': webhookStatusUrl!, + 'sdkType': sdkType, + 'sdkVersion': sdkVersion, + 'variant': mobileSdkVariant, }; + + if (useSendCryptoCallback == true) { + queryParameters['useSendCryptoCallbackVersion'] = '1'; + } + + return base.replace( + scheme: base.scheme.isEmpty ? 'https' : base.scheme, + path: base.path.isEmpty ? '/' : base.path, + queryParameters: queryParameters, + ); } + + static bool _nonEmpty(String? value) => value != null && value.isNotEmpty; } diff --git a/lib/internal/ramp_webview_controller.dart b/lib/internal/ramp_webview_controller.dart new file mode 100644 index 0000000..c56c827 --- /dev/null +++ b/lib/internal/ramp_webview_controller.dart @@ -0,0 +1,193 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_android/webview_flutter_android.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; + +import '../offramp_sale.dart'; +import '../onramp_purchase.dart'; +import '../send_crypto_payload.dart'; + +/// Owns the widget WebView and forwards Ramp Instant JS events to Dart callbacks. +class RampWebViewController { + RampWebViewController(this._widgetUrl); + + static const _channelName = 'RampInstantMobile'; + + final Uri _widgetUrl; + WebViewController? _webView; + + Function(OnrampPurchase, String, String)? onOnrampPurchaseCreated; + Function(OfframpSale, String, String)? onOfframpSaleCreated; + Function(SendCryptoPayload)? onSendCryptoRequested; + Function()? onClosed; + + WebViewController get webViewController => + _webView ??= _createWebViewController(); + + WebViewController _createWebViewController() { + final PlatformWebViewControllerCreationParams params; + if (WebViewPlatform.instance is WebKitWebViewPlatform) { + params = WebKitWebViewControllerCreationParams( + allowsInlineMediaPlayback: true, + ); + } else { + params = const PlatformWebViewControllerCreationParams(); + } + + final controller = WebViewController.fromPlatformCreationParams(params) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onNavigationRequest: (request) => _onNavigationRequest(request), + ), + ) + ..addJavaScriptChannel( + _channelName, + onMessageReceived: (message) => handleJavaScriptMessage(message.message), + ) + ..setOnPlatformPermissionRequest((request) { + final onlyCamera = request.types.every( + (type) => type == WebViewPermissionResourceType.camera, + ); + if (onlyCamera) { + request.grant(); + } else { + request.deny(); + } + }); + + final platform = controller.platform; + if (platform is AndroidWebViewController) { + platform.setMediaPlaybackRequiresUserGesture(false); + // Document upload on Android needs a host-provided file picker; not wired + // here. KYC camera is handled via setOnPlatformPermissionRequest. + } + + controller.loadRequest(_widgetUrl); + return controller; + } + + Future _onNavigationRequest( + NavigationRequest request, + ) async { + final uri = Uri.tryParse(request.url); + if (uri == null) { + return NavigationDecision.prevent; + } + + if (_isWidgetNavigation(uri)) { + return NavigationDecision.navigate; + } + + await _openExternal(uri); + return NavigationDecision.prevent; + } + + bool _isWidgetNavigation(Uri uri) { + if (uri.scheme == 'about') return true; + if (uri.host.isEmpty) return true; + if (uri.host == _widgetUrl.host) return true; + // Allow same-site Ramp hosts during widget redirects. + return _trustedRampHost.hasMatch(uri.host); + } + + Future _openExternal(Uri uri) async { + try { + if (uri.scheme == 'intent') { + final fallback = _intentFallbackUrl(uri); + if (fallback != null) { + await launchUrl(fallback, mode: LaunchMode.externalApplication); + } + return; + } + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } catch (error, stackTrace) { + debugPrint('RampFlutter: failed to open $uri: $error\n$stackTrace'); + } + } + + /// Extracts a browser URL from an Android `intent://` URI when present. + static Uri? _intentFallbackUrl(Uri intentUri) { + final browserFallback = intentUri.queryParameters['browser_fallback_url']; + if (browserFallback != null && browserFallback.isNotEmpty) { + return Uri.tryParse(browserFallback); + } + return null; + } + + Future sendCrypto(String? transactionHash) { + final webView = _webView; + if (webView == null) { + return Future.value(); + } + final message = jsonEncode({ + 'type': 'SEND_CRYPTO_RESULT', + 'eventVersion': 1, + 'payload': {'txHash': transactionHash}, + }); + return webView.runJavaScript( + 'window.postMessage($message, "${_widgetUrl.origin}");', + ); + } + + /// Stops the page and releases capture. Call when the route is dismissed. + void dispose() { + _webView + ?..removeJavaScriptChannel(_channelName) + ..loadRequest(Uri.parse('about:blank')); + _webView = null; + } + + @visibleForTesting + void handleJavaScriptMessage(String message) { + final dynamic event; + try { + event = jsonDecode(message); + } on FormatException { + return; + } + if (event is! Map) return; + final eventMap = Map.from(event); + + final payload = eventMap['payload']; + switch (eventMap['type']) { + case 'PURCHASE_CREATED': + if (payload is! Map) return; + final payloadMap = Map.from(payload); + onOnrampPurchaseCreated?.call( + OnrampPurchase.fromArguments(payloadMap['purchase']), + payloadMap['purchaseViewToken'] as String? ?? '', + payloadMap['apiUrl'] as String? ?? '', + ); + break; + case 'OFFRAMP_SALE_CREATED': + if (payload is! Map) return; + final payloadMap = Map.from(payload); + onOfframpSaleCreated?.call( + OfframpSale.fromArguments(payloadMap['sale']), + payloadMap['saleViewToken'] as String? ?? '', + payloadMap['apiUrl'] as String? ?? '', + ); + break; + case 'SEND_CRYPTO': + onSendCryptoRequested?.call( + SendCryptoPayload.fromArguments(payload), + ); + break; + case 'CLOSE': + case 'WIDGET_CLOSE': + onClosed?.call(); + break; + } + } +} + +final _trustedRampHost = RegExp( + r'^([a-z0-9-]+\.)*(ramp\.network|rampnetwork\.com|ramp-network\.org)$', +); diff --git a/lib/internal/ramp_webview_page.dart b/lib/internal/ramp_webview_page.dart new file mode 100644 index 0000000..423e6c7 --- /dev/null +++ b/lib/internal/ramp_webview_page.dart @@ -0,0 +1,16 @@ +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +import 'ramp_webview_controller.dart'; + +/// Fullscreen page that hosts the Ramp widget WebView. +class RampWebViewPage extends StatelessWidget { + const RampWebViewPage({super.key, required this.controller}); + + final RampWebViewController controller; + + @override + Widget build(BuildContext context) { + return WebViewWidget(controller: controller.webViewController); + } +} diff --git a/lib/offramp_sale.dart b/lib/offramp_sale.dart index 05080d9..2fab5f2 100644 --- a/lib/offramp_sale.dart +++ b/lib/offramp_sale.dart @@ -20,6 +20,7 @@ class OfframpCrypto { static OfframpCrypto fromArguments(dynamic arguments) { OfframpCrypto crypto = OfframpCrypto(); + if (arguments == null) return crypto; crypto.amount = arguments["amount"]; crypto.assetInfo = OfframpAssetInfo.fromArguments(arguments["assetInfo"]); return crypto; @@ -35,6 +36,7 @@ class OfframpAssetInfo { static OfframpAssetInfo fromArguments(dynamic arguments) { OfframpAssetInfo assetInfo = OfframpAssetInfo(); + if (arguments == null) return assetInfo; assetInfo.chain = arguments["chain"]; assetInfo.decimals = arguments["decimals"]; assetInfo.name = arguments["name"]; @@ -50,7 +52,8 @@ class OfframpFiat { static OfframpFiat fromArguments(dynamic arguments) { OfframpFiat fiat = OfframpFiat(); - fiat.amount = arguments["amount"]; + if (arguments == null) return fiat; + fiat.amount = (arguments["amount"] as num?)?.toDouble(); fiat.currencySymbol = arguments["currencySymbol"]; return fiat; } diff --git a/lib/onramp_purchase.dart b/lib/onramp_purchase.dart index 2e6d79b..a90e101 100644 --- a/lib/onramp_purchase.dart +++ b/lib/onramp_purchase.dart @@ -5,7 +5,7 @@ class OnrampPurchase { String? receiverAddress; String? cryptoAmount; String? fiatCurrency; - int? fiatValue; + double? fiatValue; double? assetExchangeRate; double? baseRampFee; double? networkFee; @@ -26,11 +26,11 @@ class OnrampPurchase { purchase.receiverAddress = arguments["receiverAddress"]; purchase.cryptoAmount = arguments["cryptoAmount"]; purchase.fiatCurrency = arguments["fiatCurrency"]; - purchase.fiatValue = arguments["fiatValue"]; - purchase.assetExchangeRate = arguments["assetExchangeRate"]; - purchase.baseRampFee = arguments["baseRampFee"]; - purchase.networkFee = arguments["networkFee"]; - purchase.appliedFee = arguments["appliedFee"]; + purchase.fiatValue = _toDouble(arguments["fiatValue"]); + purchase.assetExchangeRate = _toDouble(arguments["assetExchangeRate"]); + purchase.baseRampFee = _toDouble(arguments["baseRampFee"]); + purchase.networkFee = _toDouble(arguments["networkFee"]); + purchase.appliedFee = _toDouble(arguments["appliedFee"]); purchase.paymentMethodType = arguments["paymentMethodType"]; purchase.finalTxHash = arguments["finalTxHash"]; purchase.createdAt = arguments["createdAt"]; @@ -42,6 +42,8 @@ class OnrampPurchase { } } +double? _toDouble(dynamic value) => (value as num?)?.toDouble(); + class PurchaseAssetInfo { String? address; int? decimals; @@ -51,6 +53,7 @@ class PurchaseAssetInfo { static PurchaseAssetInfo fromArguments(dynamic arguments) { PurchaseAssetInfo assetInfo = PurchaseAssetInfo(); + if (arguments == null) return assetInfo; assetInfo.address = arguments["address"]; assetInfo.decimals = arguments["decimals"]; assetInfo.name = arguments["name"]; diff --git a/lib/ramp_flutter.dart b/lib/ramp_flutter.dart index be3be42..33e0193 100644 --- a/lib/ramp_flutter.dart +++ b/lib/ramp_flutter.dart @@ -1,72 +1,61 @@ -import 'dart:async'; - -import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:ramp_flutter/configuration.dart'; +import 'package:ramp_flutter/internal/ramp_webview_controller.dart'; +import 'package:ramp_flutter/internal/ramp_webview_page.dart'; import 'package:ramp_flutter/offramp_sale.dart'; import 'package:ramp_flutter/onramp_purchase.dart'; import 'package:ramp_flutter/send_crypto_payload.dart'; -import 'configuration.dart'; - -/// Wrapper class for Ramp Network Flutter widget +/// Flutter API for presenting the Ramp Network widget. class RampFlutter { - final MethodChannel _channel = const MethodChannel('ramp_flutter'); + RampWebViewController? _activeController; Function(OnrampPurchase, String, String)? onOnrampPurchaseCreated; Function(SendCryptoPayload payload)? onSendCryptoRequested; Function(OfframpSale, String, String)? onOfframpSaleCreated; Function()? onRampClosed; - void _handleOnOnrampPurchaseCreated(dynamic arguments) { - dynamic payload = arguments[0]; - String purchaseViewToken = arguments[1]; - String apiUrl = arguments[2]; - OnrampPurchase purchase = OnrampPurchase.fromArguments(payload); - onOnrampPurchaseCreated!(purchase, purchaseViewToken, apiUrl); - } - - void _handleOnSendCryptoRequested(dynamic arguments) { - dynamic payload = arguments[0]; - SendCryptoPayload sendCrypto = SendCryptoPayload.fromArguments(payload); - onSendCryptoRequested!(sendCrypto); - } - - void _handleOnOfframpSaleCreated(dynamic arguments) { - dynamic payload = arguments[0]; - String saleViewToken = arguments[1]; - String apiUrl = arguments[2]; - OfframpSale sale = OfframpSale.fromArguments(payload); - onOfframpSaleCreated!(sale, saleViewToken, apiUrl); - } - - void _handleOnRampClosed() { - onRampClosed!(); - } - - Future _didRecieveMethodCall(MethodCall call) async { - switch (call.method) { - case "onOnrampPurchaseCreated": - _handleOnOnrampPurchaseCreated(call.arguments); - break; - case "onSendCryptoRequested": - _handleOnSendCryptoRequested(call.arguments); - break; - case "onOfframpSaleCreated": - _handleOnOfframpSaleCreated(call.arguments); - break; - case "onRampClosed": - _handleOnRampClosed(); - break; - } - } - + /// Builds the widget URL from [configuration] and pushes a fullscreen route. Future showRamp( + BuildContext context, Configuration configuration, ) async { - _channel.setMethodCallHandler(_didRecieveMethodCall); - await _channel.invokeMethod('showRamp', configuration.toMap()); + final navigator = Navigator.of(context, rootNavigator: true); + final widgetUrl = configuration.buildWidgetUrl(); + final controller = RampWebViewController(widgetUrl) + ..onOnrampPurchaseCreated = onOnrampPurchaseCreated + ..onOfframpSaleCreated = onOfframpSaleCreated + ..onSendCryptoRequested = onSendCryptoRequested; + + _activeController = controller; + + controller.onClosed = () { + if (navigator.canPop()) { + navigator.pop(); + } + }; + + await navigator.push( + PageRouteBuilder( + opaque: true, + pageBuilder: (context, animation, secondaryAnimation) { + return RampWebViewPage(controller: controller); + }, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition(opacity: animation, child: child); + }, + ), + ); + + controller.dispose(); + if (identical(_activeController, controller)) { + _activeController = null; + } + onRampClosed?.call(); } + /// Completes an off-ramp send-crypto request with an optional [transactionHash]. Future sendCrypto(String? transactionHash) async { - await _channel.invokeMethod('sendCrypto', transactionHash); + await _activeController?.sendCrypto(transactionHash); } } diff --git a/lib/send_crypto_payload.dart b/lib/send_crypto_payload.dart index 46f54d9..1ba73b1 100644 --- a/lib/send_crypto_payload.dart +++ b/lib/send_crypto_payload.dart @@ -5,20 +5,13 @@ class SendCryptoPayload { static SendCryptoPayload fromArguments(dynamic arguments) { SendCryptoPayload payload = SendCryptoPayload(); + if (arguments == null) return payload; payload.address = arguments["address"]; payload.amount = arguments["amount"]; payload.assetInfo = SendCryptoAssetInfo.fromArguments(arguments["assetInfo"]); return payload; } - - dynamic toMap() { - return { - 'address': address, - 'amount': amount, - 'assetInfo': assetInfo, - }; - } } class SendCryptoAssetInfo { @@ -30,6 +23,7 @@ class SendCryptoAssetInfo { static SendCryptoAssetInfo fromArguments(dynamic arguments) { SendCryptoAssetInfo payload = SendCryptoAssetInfo(); + if (arguments == null) return payload; payload.chain = arguments["chain"]; payload.decimals = arguments["decimals"]; payload.name = arguments["name"]; @@ -37,14 +31,4 @@ class SendCryptoAssetInfo { payload.type = arguments["type"]; return payload; } - - dynamic toMap() { - return { - 'chain': chain, - 'decimals': decimals, - 'name': name, - 'symbol': symbol, - 'type': type, - }; - } } diff --git a/pubspec.lock b/pubspec.lock index d512e20..d4ae7ba 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,22 +1,54 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -30,6 +62,40 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" lints: dependency: transitive description: @@ -38,24 +104,40 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.18.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" plugin_platform_interface: - dependency: "direct main" + dependency: transitive description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" @@ -66,15 +148,175 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 + url: "https://pub.dev" + source: hosted + version: "6.3.32" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736 + url: "https://pub.dev" + source: hosted + version: "4.9.0" + webview_flutter_android: + dependency: "direct main" + description: + name: webview_flutter_android + sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558" + url: "https://pub.dev" + source: hosted + version: "3.16.9" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" + url: "https://pub.dev" + source: hosted + version: "2.15.1" + webview_flutter_wkwebview: + dependency: "direct main" + description: + name: webview_flutter_wkwebview + sha256: c879dd64b87c452aa84381b244d5469da57ba7e8cca6884c7b1e0d406372c12d + url: "https://pub.dev" + source: hosted + version: "3.26.0" sdks: - dart: ">=3.3.0-0 <4.0.0" - flutter: ">=3.3.0" + dart: ">=3.12.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index 49c642d..f0fb064 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: ramp_flutter -description: Ramp Network for Flutter is a simple wrapper for native iOS and Android Ramp Network SDKs. Unified API lets you write code once and use on any of the platforms. Ramp Network Flutter SDK supports iOS and Android platforms. -version: 4.0.1 +description: Ramp Network for Flutter loads the Ramp widget in a Flutter WebView with a unified Dart API for iOS and Android. +version: 5.0.0 homepage: https://ramp.network/ environment: @@ -10,10 +10,15 @@ environment: dependencies: flutter: sdk: flutter - plugin_platform_interface: ^2.0.2 + url_launcher: ^6.2.5 + webview_flutter: ^4.2.4 + webview_flutter_android: ^3.16.0 + webview_flutter_wkwebview: ^3.14.0 dev_dependencies: flutter_lints: ^2.0.0 + flutter_test: + sdk: flutter flutter: plugin: diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart new file mode 100644 index 0000000..b4381e2 --- /dev/null +++ b/test/ramp_webview_test.dart @@ -0,0 +1,154 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:ramp_flutter/configuration.dart'; +import 'package:ramp_flutter/internal/ramp_webview_controller.dart'; +import 'package:ramp_flutter/offramp_sale.dart'; +import 'package:ramp_flutter/onramp_purchase.dart'; +import 'package:ramp_flutter/send_crypto_payload.dart'; + +void main() { + group('Configuration.buildWidgetUrl', () { + test('uses default base URL and SDK metadata', () { + final url = Configuration().buildWidgetUrl(); + + expect(url.scheme, 'https'); + expect(url.host, 'app.rampnetwork.com'); + expect(url.queryParameters['sdkType'], 'Flutter'); + expect(url.queryParameters['sdkVersion'], '5.0.0'); + expect(url.queryParameters['variant'], 'sdk-mobile'); + }); + + test('merges configuration fields and joins enabled flows', () { + final url = (Configuration() + ..url = 'https://app.dev.ramp-network.org/custom' + ..hostApiKey = 'key' + ..hostAppName = 'App' + ..offrampAsset = 'ETH' + ..offrampWebhookV3Url = 'https://example.com/hook' + ..enabledFlows = ['ONRAMP', 'OFFRAMP'] + ..defaultFlow = 'OFFRAMP' + ..useSendCryptoCallback = true + ..variant = 'ignored') + .buildWidgetUrl(); + + expect(url.host, 'app.dev.ramp-network.org'); + expect(url.path, '/custom'); + expect(url.queryParameters['hostApiKey'], 'key'); + expect(url.queryParameters['hostAppName'], 'App'); + expect(url.queryParameters['offrampAsset'], 'ETH'); + expect(url.queryParameters['offrampWebhookV3Url'], + 'https://example.com/hook'); + expect(url.queryParameters['enabledFlows'], 'ONRAMP,OFFRAMP'); + expect(url.queryParameters['defaultFlow'], 'OFFRAMP'); + expect(url.queryParameters['useSendCryptoCallbackVersion'], '1'); + expect(url.queryParameters['variant'], 'sdk-mobile'); + }); + + test('omits null and empty optional fields', () { + final url = (Configuration()..hostApiKey = ''..fiatValue = null) + .buildWidgetUrl(); + + expect(url.queryParameters.containsKey('hostApiKey'), isFalse); + expect(url.queryParameters.containsKey('fiatValue'), isFalse); + expect( + url.queryParameters.containsKey('useSendCryptoCallbackVersion'), + isFalse, + ); + }); + }); + + group('RampWebViewController events', () { + final widgetUrl = Uri.parse('https://app.rampnetwork.com/'); + + test('forwards purchase events with numeric coercion', () { + OnrampPurchase? purchase; + String? token; + String? apiUrl; + final controller = RampWebViewController(widgetUrl) + ..onOnrampPurchaseCreated = (p, t, url) { + purchase = p; + token = t; + apiUrl = url; + }; + + controller.handleJavaScriptMessage(jsonEncode({ + 'type': 'PURCHASE_CREATED', + 'payload': { + 'purchase': { + 'id': 'purchase-id', + 'asset': {'decimals': 18, 'name': 'Ether', 'symbol': 'ETH'}, + 'fiatValue': 100.5, + 'assetExchangeRate': 2, + }, + 'purchaseViewToken': 'view-token', + 'apiUrl': 'https://api.ramp.network', + }, + })); + + expect(purchase?.id, 'purchase-id'); + expect(token, 'view-token'); + expect(apiUrl, 'https://api.ramp.network'); + expect(purchase?.fiatValue, 100.5); + expect(purchase?.assetExchangeRate, 2.0); + }); + + test('forwards sale events with whole-number fiat amounts', () { + OfframpSale? sale; + final controller = RampWebViewController(widgetUrl) + ..onOfframpSaleCreated = (s, token, url) => sale = s; + + controller.handleJavaScriptMessage(jsonEncode({ + 'type': 'OFFRAMP_SALE_CREATED', + 'payload': { + 'sale': { + 'id': 'sale-id', + 'crypto': { + 'amount': '1000000000000000000', + 'assetInfo': { + 'chain': 'ETH', + 'decimals': 18, + 'name': 'Ether', + 'symbol': 'ETH', + }, + }, + 'fiat': {'amount': 100, 'currencySymbol': 'EUR'}, + }, + 'saleViewToken': 'view-token', + 'apiUrl': 'https://api.ramp.network', + }, + })); + + expect(sale?.id, 'sale-id'); + expect(sale?.fiat?.amount, 100.0); + expect(sale?.crypto?.assetInfo?.chain, 'ETH'); + }); + + test('forwards send crypto requests and both close events', () { + SendCryptoPayload? payload; + var closedCount = 0; + final controller = RampWebViewController(widgetUrl) + ..onSendCryptoRequested = (p) { + payload = p; + } + ..onClosed = () => closedCount++; + + controller.handleJavaScriptMessage('not json'); + controller.handleJavaScriptMessage('42'); + controller.handleJavaScriptMessage(jsonEncode({ + 'type': 'SEND_CRYPTO', + 'payload': { + 'address': '0xabc', + 'amount': '1', + 'assetInfo': {'chain': 'ETH'}, + }, + })); + controller.handleJavaScriptMessage(jsonEncode({'type': 'CLOSE'})); + controller.handleJavaScriptMessage(jsonEncode({'type': 'WIDGET_CLOSE'})); + + expect(payload?.address, '0xabc'); + expect(payload?.assetInfo?.chain, 'ETH'); + expect(closedCount, 2); + }); + }); +} From aed8065ce2c805745fd88dff058a570b1d372392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 11:53:55 +0200 Subject: [PATCH 02/56] convert to flutter package, drop redundant mobile shells --- .gitignore | 2 + .metadata | 25 +--------- CHANGELOG.md | 3 +- README.md | 9 ++-- android/.gitignore | 9 ---- android/build.gradle | 50 ------------------- android/settings.gradle | 1 - android/src/main/AndroidManifest.xml | 3 -- .../ramp/ramp_flutter/RampFlutterPlugin.kt | 10 ---- example/pubspec.yaml | 2 +- ios/.gitignore | 38 -------------- ios/Assets/.gitkeep | 0 ios/Classes/RampFlutterPlugin.swift | 7 --- ios/ramp_flutter.podspec | 20 -------- pubspec.yaml | 9 ---- 15 files changed, 11 insertions(+), 177 deletions(-) delete mode 100644 android/.gitignore delete mode 100644 android/build.gradle delete mode 100644 android/settings.gradle delete mode 100644 android/src/main/AndroidManifest.xml delete mode 100644 android/src/main/kotlin/network/ramp/ramp_flutter/RampFlutterPlugin.kt delete mode 100644 ios/.gitignore delete mode 100644 ios/Assets/.gitkeep delete mode 100644 ios/Classes/RampFlutterPlugin.swift delete mode 100644 ios/ramp_flutter.podspec diff --git a/.gitignore b/.gitignore index ac5aa98..eb6c05c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ migrate_working_dir/ /pubspec.lock **/doc/api/ .dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies build/ diff --git a/.metadata b/.metadata index 45cf2b7..9d07399 100644 --- a/.metadata +++ b/.metadata @@ -7,27 +7,4 @@ version: revision: "46787ee49c1fd80ab603f0702733edab54653aef" channel: "stable" -project_type: plugin - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: 46787ee49c1fd80ab603f0702733edab54653aef - base_revision: 46787ee49c1fd80ab603f0702733edab54653aef - - platform: android - create_revision: 46787ee49c1fd80ab603f0702733edab54653aef - base_revision: 46787ee49c1fd80ab603f0702733edab54653aef - - platform: ios - create_revision: 46787ee49c1fd80ab603f0702733edab54653aef - base_revision: 46787ee49c1fd80ab603f0702733edab54653aef - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' +project_type: package diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e0d89c..7306dfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ ## 5.0.0 * Rewrite the SDK to load the Ramp widget in a Flutter WebView instead of the - native iOS/Android Ramp SDKs. + native iOS/Android Ramp SDKs. Published as a Dart Flutter package (no + Android/iOS plugin shells). * Breaking: `showRamp` now requires a `BuildContext` and pushes a fullscreen route (`showRamp(context, configuration)`). * Add `Configuration.offrampAsset` and build the widget URL in Dart diff --git a/README.md b/README.md index a9d3a00..5c8ba92 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Ramp Network Flutter -Official Flutter SDK for Ramp Network. Loads the Ramp widget in a Flutter -WebView on iOS and Android. +Official Flutter package for Ramp Network. Loads the Ramp widget in a Flutter +WebView on iOS and Android (no native Ramp SDK or Flutter plugin shells). ## Getting Started @@ -18,8 +18,9 @@ dependencies: need KYC camera capture. - **iOS:** deployment target 13+. Add `NSCameraUsageDescription` (and photo library usage if you rely on document upload). -- Native Ramp iOS/Android SDKs are **not** required (no CocoaPods `Ramp` pod, - no JitPack `ramp-sdk-android`). +- Native Ramp iOS/Android SDKs and empty Flutter plugin shells are **not** + required (no CocoaPods `Ramp` pod, no JitPack `ramp-sdk-android`). + Platform WebView support comes from `webview_flutter` / `url_launcher`. ### Usage diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index 161bdcd..0000000 --- a/android/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -.cxx diff --git a/android/build.gradle b/android/build.gradle deleted file mode 100644 index bb5d422..0000000 --- a/android/build.gradle +++ /dev/null @@ -1,50 +0,0 @@ -group 'network.ramp.ramp_flutter' -version '1.0-SNAPSHOT' - -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - if (project.android.hasProperty("namespace")) { - namespace 'network.ramp.ramp_flutter' - } - - compileSdkVersion 33 - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - minSdkVersion 24 - } -} diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index fc7d53b..0000000 --- a/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'ramp_flutter' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml deleted file mode 100644 index bcb4ebe..0000000 --- a/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/android/src/main/kotlin/network/ramp/ramp_flutter/RampFlutterPlugin.kt b/android/src/main/kotlin/network/ramp/ramp_flutter/RampFlutterPlugin.kt deleted file mode 100644 index a4e72f9..0000000 --- a/android/src/main/kotlin/network/ramp/ramp_flutter/RampFlutterPlugin.kt +++ /dev/null @@ -1,10 +0,0 @@ -package network.ramp.ramp_flutter - -import io.flutter.embedding.engine.plugins.FlutterPlugin - -/** No-op plugin registration; widget flow is implemented in Dart via WebView. */ -class RampFlutterPlugin : FlutterPlugin { - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {} - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {} -} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 2351647..07353dd 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,5 +1,5 @@ name: ramp_flutter_example -description: "Demonstrates how to use the ramp_flutter plugin." +description: "Demonstrates how to use the ramp_flutter package." version: 1.0.0+1 publish_to: 'none' # Remove this line if you wish to publish to pub.dev diff --git a/ios/.gitignore b/ios/.gitignore deleted file mode 100644 index 0c88507..0000000 --- a/ios/.gitignore +++ /dev/null @@ -1,38 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/ephemeral/ -/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/ios/Assets/.gitkeep b/ios/Assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/ios/Classes/RampFlutterPlugin.swift b/ios/Classes/RampFlutterPlugin.swift deleted file mode 100644 index 429d37a..0000000 --- a/ios/Classes/RampFlutterPlugin.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Flutter -import UIKit - -/// No-op plugin registration; widget flow is implemented in Dart via WebView. -public class RampFlutterPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) {} -} diff --git a/ios/ramp_flutter.podspec b/ios/ramp_flutter.podspec deleted file mode 100644 index 801d4e6..0000000 --- a/ios/ramp_flutter.podspec +++ /dev/null @@ -1,20 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'ramp_flutter' - s.version = '5.0.0' - s.summary = 'Ramp Network Flutter SDK.' - s.description = <<-DESC - Ramp Network for Flutter loads the Ramp widget in a Flutter WebView with a - unified Dart API for iOS and Android. - DESC - s.homepage = 'https://docs.ramp.network/mobile/flutter-sdk/' - s.license = 'MIT' - s.author = { 'Ramp Network' => 'dev@ramp.network' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'Flutter' - s.platform = :ios, '13.0' - - # Flutter.framework does not contain a i386 slice. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } - s.swift_version = '5.0' -end diff --git a/pubspec.yaml b/pubspec.yaml index f0fb064..bf24696 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,12 +19,3 @@ dev_dependencies: flutter_lints: ^2.0.0 flutter_test: sdk: flutter - -flutter: - plugin: - platforms: - android: - package: network.ramp.ramp_flutter - pluginClass: RampFlutterPlugin - ios: - pluginClass: RampFlutterPlugin From 6ca62dcbdb3bf1520bc52485d24541b0d77b9ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 12:04:18 +0200 Subject: [PATCH 03/56] fix webview permissions and example --- README.md | 5 +- example/.gitignore | 2 + example/ios/Flutter/AppFrameworkInfo.plist | 2 - example/ios/Podfile.lock | 26 ++------ example/ios/Runner.xcodeproj/project.pbxproj | 24 +++++++- .../xcshareddata/xcschemes/Runner.xcscheme | 23 ++++++- example/ios/Runner/AppDelegate.swift | 11 ++-- example/ios/Runner/Info.plist | 21 +++++++ example/lib/main.dart | 6 +- lib/internal/ramp_webview_controller.dart | 28 +++++---- lib/ramp_flutter.dart | 60 +++++++++++++------ 11 files changed, 141 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 5c8ba92..ac06e7a 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,9 @@ final configuration = Configuration() await ramp.showRamp(context, configuration); ``` -`showRamp` requires a `BuildContext` with a `Navigator` and presents a -fullscreen WebView route. +`showRamp` uses Flutter's [showModalBottomSheet](https://api.flutter.dev/flutter/material/showModalBottomSheet.html). +The host app must provide a `MaterialApp` (or equivalent `MaterialLocalizations`) +above the `BuildContext` you pass in. Swipe down from the drag handle to dismiss. For more configuration parameters see [Ramp Network Flutter documentation](https://docs.ramp.network/mobile/flutter-sdk/). diff --git a/example/.gitignore b/example/.gitignore index 29a3a50..79c113f 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 7c56964..391a902 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 12.0 diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 2bede9f..c56b8fc 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2,39 +2,21 @@ PODS: - Flutter (1.0.0) - flutter_local_notifications (0.0.1): - Flutter - - Ramp (4.0.2) - - ramp_flutter (1.0.1): - - Flutter - - Ramp DEPENDENCIES: - Flutter (from `Flutter`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - - Ramp (from `https://github.com/RampNetwork/ramp-sdk-ios`, tag `4.0.2`) - - ramp_flutter (from `.symlinks/plugins/ramp_flutter/ios`) EXTERNAL SOURCES: Flutter: :path: Flutter flutter_local_notifications: :path: ".symlinks/plugins/flutter_local_notifications/ios" - Ramp: - :git: https://github.com/RampNetwork/ramp-sdk-ios - :tag: 4.0.2 - ramp_flutter: - :path: ".symlinks/plugins/ramp_flutter/ios" - -CHECKOUT OPTIONS: - Ramp: - :git: https://github.com/RampNetwork/ramp-sdk-ios - :tag: 4.0.2 SPEC CHECKSUMS: - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 - Ramp: fb07b544581cc9ad0eb8b56b43c9d59ef6204ca2 - ramp_flutter: 412e68fa0ee3196fc043f83555534574796edf2f + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100 -PODFILE CHECKSUM: 15079d41652eeb2a8525b2965db898b1e2e9e3ca +PODFILE CHECKSUM: d2243213672c3c48aae53c36642ba411a6be7309 -COCOAPODS: 1.14.3 +COCOAPODS: 1.17.0 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 349ef05..be2586f 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -47,6 +48,7 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B2968B4289079AC5A10ACE82 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -54,6 +56,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 93025B5D7BBBFC1A8516BDCD /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -72,6 +75,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -128,6 +132,9 @@ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -153,10 +160,13 @@ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -553,6 +563,18 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e25afce..37e5248 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,10 +1,28 @@ + + + + + + + + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4..c30b367 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,13 +1,16 @@ -import UIKit import Flutter +import UIKit -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index f9136ac..25eb606 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -45,6 +45,27 @@ Camera access is required for identity verification in Ramp Network. NSPhotoLibraryUsageDescription Photo library access is used to upload verification documents in Ramp Network. + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName diff --git a/example/lib/main.dart b/example/lib/main.dart index ac4f0d4..85e1e56 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -99,10 +99,10 @@ class _RampFlutterAppState extends State { @override Widget build(BuildContext context) { - return PlatformApp( + return MaterialApp( home: Builder( - builder: (context) => PlatformScaffold( - appBar: PlatformAppBar( + builder: (context) => Scaffold( + appBar: AppBar( title: const Text('Ramp Network Flutter'), ), body: Padding( diff --git a/lib/internal/ramp_webview_controller.dart b/lib/internal/ramp_webview_controller.dart index c56c827..c2fb997 100644 --- a/lib/internal/ramp_webview_controller.dart +++ b/lib/internal/ramp_webview_controller.dart @@ -38,7 +38,19 @@ class RampWebViewController { params = const PlatformWebViewControllerCreationParams(); } - final controller = WebViewController.fromPlatformCreationParams(params) + final controller = WebViewController.fromPlatformCreationParams( + params, + onPermissionRequest: (request) { + final onlyCamera = request.types.every( + (type) => type == WebViewPermissionResourceType.camera, + ); + if (onlyCamera) { + request.grant(); + } else { + request.deny(); + } + }, + ) ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( @@ -48,23 +60,13 @@ class RampWebViewController { ..addJavaScriptChannel( _channelName, onMessageReceived: (message) => handleJavaScriptMessage(message.message), - ) - ..setOnPlatformPermissionRequest((request) { - final onlyCamera = request.types.every( - (type) => type == WebViewPermissionResourceType.camera, - ); - if (onlyCamera) { - request.grant(); - } else { - request.deny(); - } - }); + ); final platform = controller.platform; if (platform is AndroidWebViewController) { platform.setMediaPlaybackRequiresUserGesture(false); // Document upload on Android needs a host-provided file picker; not wired - // here. KYC camera is handled via setOnPlatformPermissionRequest. + // here. KYC camera is handled via onPermissionRequest. } controller.loadRequest(_widgetUrl); diff --git a/lib/ramp_flutter.dart b/lib/ramp_flutter.dart index 33e0193..1e19f40 100644 --- a/lib/ramp_flutter.dart +++ b/lib/ramp_flutter.dart @@ -1,4 +1,4 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:ramp_flutter/configuration.dart'; import 'package:ramp_flutter/internal/ramp_webview_controller.dart'; import 'package:ramp_flutter/internal/ramp_webview_page.dart'; @@ -15,12 +15,15 @@ class RampFlutter { Function(OfframpSale, String, String)? onOfframpSaleCreated; Function()? onRampClosed; - /// Builds the widget URL from [configuration] and pushes a fullscreen route. + /// Builds the widget URL from [configuration] and presents it in a + /// [showModalBottomSheet]. + /// + /// Requires a [MaterialApp] (or other ancestor that provides + /// [MaterialLocalizations]) above [context]. Future showRamp( BuildContext context, Configuration configuration, ) async { - final navigator = Navigator.of(context, rootNavigator: true); final widgetUrl = configuration.buildWidgetUrl(); final controller = RampWebViewController(widgetUrl) ..onOnrampPurchaseCreated = onOnrampPurchaseCreated @@ -29,22 +32,41 @@ class RampFlutter { _activeController = controller; - controller.onClosed = () { - if (navigator.canPop()) { - navigator.pop(); - } - }; - - await navigator.push( - PageRouteBuilder( - opaque: true, - pageBuilder: (context, animation, secondaryAnimation) { - return RampWebViewPage(controller: controller); - }, - transitionsBuilder: (context, animation, secondaryAnimation, child) { - return FadeTransition(opacity: animation, child: child); - }, - ), + await showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + enableDrag: true, + isDismissible: true, + useSafeArea: true, + builder: (sheetContext) { + controller.onClosed = () { + if (Navigator.of(sheetContext).canPop()) { + Navigator.of(sheetContext).pop(); + } + }; + + return SizedBox( + height: MediaQuery.sizeOf(sheetContext).height * 0.92, + child: Column( + children: [ + const SizedBox( + height: 28, + child: Center( + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.all(Radius.circular(2)), + ), + child: SizedBox(width: 36, height: 4), + ), + ), + ), + Expanded(child: RampWebViewPage(controller: controller)), + ], + ), + ); + }, ); controller.dispose(); From c57351f85b54fab7b58538d486f71569132d9cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 12:15:28 +0200 Subject: [PATCH 04/56] fix host api loading --- example/.gitignore | 3 ++ example/README.md | 8 +++ example/lib/main.dart | 61 +++++++++++++++++------ example/lib/secrets.example.dart | 8 +++ lib/configuration.dart | 2 +- lib/internal/ramp_webview_controller.dart | 20 ++++++++ test/ramp_webview_test.dart | 2 +- 7 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 example/lib/secrets.example.dart diff --git a/example/.gitignore b/example/.gitignore index 79c113f..f9d52b9 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -33,6 +33,9 @@ migrate_working_dir/ .pub/ /build/ +# Local example secrets (copy from secrets.example.dart) +lib/secrets.dart + # Symbolication related app.*.symbols diff --git a/example/README.md b/example/README.md index 08f0827..a3aaef8 100644 --- a/example/README.md +++ b/example/README.md @@ -3,3 +3,11 @@ Demonstrates the Ramp Network Flutter WebView SDK (`ramp_flutter` 5.0.0). Use **Show Ramp** to present the widget via `ramp.showRamp(context, configuration)`. + +## Local secrets + +Copy the template and add your host API key (file is gitignored): + +```bash +cp lib/secrets.example.dart lib/secrets.dart +``` diff --git a/example/lib/main.dart b/example/lib/main.dart index 85e1e56..ad75b5f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -9,6 +9,8 @@ import 'package:ramp_flutter/send_crypto_payload.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'secrets.dart'; + void main() { WidgetsFlutterBinding.ensureInitialized(); _setupNotifications(); @@ -43,20 +45,23 @@ class _RampFlutterAppState extends State { final List _predefinedEnvironments = [ "https://app.dev.ramp-network.org", - "https://ri-widget-staging.firebaseapp.com", + "https://app.demo.ramp.network", "https://app.rampnetwork.com", ]; - int _selectedEnvironment = 1; + int _selectedEnvironment = 0; @override void initState() { _configuration.hostAppName = "Ramp Network Flutter"; - _configuration.hostLogoUrl = "https://ramp.network/assets/images/Logo.svg"; - _configuration.url = _predefinedEnvironments[_selectedEnvironment]; - _configuration.enabledFlows = ["ONRAMP", "OFFRAMP"]; + _configuration.hostLogoUrl = + "https://assets.rampnetwork.com/misc/ramp-network-logo.svg"; + _configuration.defaultFlow = "ONRAMP"; + _configuration.enabledFlows = ["ONRAMP", "OFFRAMP", "SWAP"]; + _configuration.defaultAsset = "BTC_BTC"; _configuration.useSendCryptoCallback = true; _configuration.deepLinkScheme = "rampflutterdemo"; + _applyEnvironment(_selectedEnvironment); ramp.onOnrampPurchaseCreated = onOnrampPurchaseCreated; ramp.onSendCryptoRequested = onSendCryptoRequested; @@ -67,11 +72,18 @@ class _RampFlutterAppState extends State { } void _selectEnvironment(int id) { - _selectedEnvironment = id; - _configuration.url = _predefinedEnvironments[_selectedEnvironment]; + _applyEnvironment(id); setState(() => {}); } + void _applyEnvironment(int id) { + _selectedEnvironment = id; + _configuration.url = _predefinedEnvironments[id]; + // Dev/internal key only; demo/prod need their own keys. + _configuration.hostApiKey = + id == 0 ? ExampleSecrets.hostApiKeyInternal : null; + } + void onOnrampPurchaseCreated( OnrampPurchase purchase, String purchaseViewToken, @@ -128,7 +140,7 @@ class _RampFlutterAppState extends State { return [ _segmentedControl( "Env:", - ["dev", "staging", "prod"], + ["dev", "demo", "prod"], _selectEnvironment, ), PlatformText( @@ -219,13 +231,32 @@ class _RampFlutterAppState extends State { setState(() => {}); }, ); - return Row(children: [ - PlatformText("Enabled: "), - PlatformText("ONRAMP"), - onRamp, - PlatformText("OFFRAMP"), - offRamp, - ]); + PlatformSwitch swap = PlatformSwitch( + value: flows.contains("SWAP"), + onChanged: (enabled) { + if (enabled) { + flows.add("SWAP"); + } else { + flows.remove("SWAP"); + } + _configuration.enabledFlows = flows; + setState(() => {}); + }, + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PlatformText("Enabled flows:"), + Row(children: [ + PlatformText("ONRAMP"), + onRamp, + PlatformText("OFFRAMP"), + offRamp, + PlatformText("SWAP"), + swap, + ]), + ], + ); } Widget _showRampButton(BuildContext context) { diff --git a/example/lib/secrets.example.dart b/example/lib/secrets.example.dart new file mode 100644 index 0000000..76a9085 --- /dev/null +++ b/example/lib/secrets.example.dart @@ -0,0 +1,8 @@ +/// Template for local example secrets. +/// +/// Copy this file to `secrets.dart` and fill in real values: +/// `cp lib/secrets.example.dart lib/secrets.dart` +class ExampleSecrets { + /// iOS app `HOST_API_KEY_INTERNAL` — used with the dev widget host. + static const String hostApiKeyInternal = 'YOUR_DEV_HOST_API_KEY'; +} diff --git a/lib/configuration.dart b/lib/configuration.dart index 9ba6ff7..ee22797 100644 --- a/lib/configuration.dart +++ b/lib/configuration.dart @@ -1,6 +1,6 @@ class Configuration { static const String defaultUrl = 'https://app.rampnetwork.com'; - static const String sdkType = 'Flutter'; + static const String sdkType = 'FLUTTER'; static const String sdkVersion = '5.0.0'; static const String mobileSdkVariant = 'sdk-mobile'; diff --git a/lib/internal/ramp_webview_controller.dart b/lib/internal/ramp_webview_controller.dart index c2fb997..7df55dc 100644 --- a/lib/internal/ramp_webview_controller.dart +++ b/lib/internal/ramp_webview_controller.dart @@ -38,6 +38,8 @@ class RampWebViewController { params = const PlatformWebViewControllerCreationParams(); } + debugPrint('RampFlutter: loading $_widgetUrl'); + final controller = WebViewController.fromPlatformCreationParams( params, onPermissionRequest: (request) { @@ -55,6 +57,21 @@ class RampWebViewController { ..setNavigationDelegate( NavigationDelegate( onNavigationRequest: (request) => _onNavigationRequest(request), + onPageStarted: (url) => debugPrint('RampFlutter: page started $url'), + onPageFinished: (url) => debugPrint('RampFlutter: page finished $url'), + onWebResourceError: (error) { + debugPrint( + 'RampFlutter: resource error ' + 'code=${error.errorCode} type=${error.errorType} ' + 'desc=${error.description} url=${error.url}', + ); + }, + onHttpError: (error) { + debugPrint( + 'RampFlutter: HTTP error ' + 'status=${error.response?.statusCode} uri=${error.request?.uri}', + ); + }, ), ) ..addJavaScriptChannel( @@ -78,13 +95,16 @@ class RampWebViewController { ) async { final uri = Uri.tryParse(request.url); if (uri == null) { + debugPrint('RampFlutter: blocking invalid navigation ${request.url}'); return NavigationDecision.prevent; } if (_isWidgetNavigation(uri)) { + debugPrint('RampFlutter: allow navigation $uri'); return NavigationDecision.navigate; } + debugPrint('RampFlutter: open external $uri'); await _openExternal(uri); return NavigationDecision.prevent; } diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index b4381e2..9a1341a 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -14,7 +14,7 @@ void main() { expect(url.scheme, 'https'); expect(url.host, 'app.rampnetwork.com'); - expect(url.queryParameters['sdkType'], 'Flutter'); + expect(url.queryParameters['sdkType'], 'FLUTTER'); expect(url.queryParameters['sdkVersion'], '5.0.0'); expect(url.queryParameters['variant'], 'sdk-mobile'); }); From 105ecf0ab03b0ef1e516e8842996288132ae82e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 12:18:27 +0200 Subject: [PATCH 05/56] drop local notifications from demo app --- example/ios/Podfile.lock | 6 -- example/ios/Runner.xcodeproj/project.pbxproj | 38 +++------ example/lib/main.dart | 47 +++-------- example/pubspec.lock | 88 -------------------- example/pubspec.yaml | 1 - 5 files changed, 24 insertions(+), 156 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index c56b8fc..587e089 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,21 +1,15 @@ PODS: - Flutter (1.0.0) - - flutter_local_notifications (0.0.1): - - Flutter DEPENDENCIES: - Flutter (from `Flutter`) - - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) EXTERNAL SOURCES: Flutter: :path: Flutter - flutter_local_notifications: - :path: ".symlinks/plugins/flutter_local_notifications/ios" SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100 PODFILE CHECKSUM: d2243213672c3c48aae53c36642ba411a6be7309 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index be2586f..aadab44 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -10,11 +10,11 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 93025B5D7BBBFC1A8516BDCD /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 089427D76D90C916BF2DEB27 /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -39,6 +39,7 @@ 5491CBB1735E80A1F7448520 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -48,7 +49,6 @@ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B2968B4289079AC5A10ACE82 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -132,9 +132,6 @@ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { - packageProductDependencies = ( - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, - ); isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( @@ -145,13 +142,15 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 71B655DF649BBEEEF639023D /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -160,9 +159,6 @@ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { - packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, - ); isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; @@ -184,6 +180,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -246,23 +245,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 71B655DF649BBEEEF639023D /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -563,12 +545,14 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; /* End XCLocalSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { isa = XCSwiftPackageProductDependency; diff --git a/example/lib/main.dart b/example/lib/main.dart index ad75b5f..905f061 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -7,31 +7,13 @@ import 'package:ramp_flutter/onramp_purchase.dart'; import 'package:ramp_flutter/ramp_flutter.dart'; import 'package:ramp_flutter/send_crypto_payload.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; - import 'secrets.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); - _setupNotifications(); runApp(const RampFlutterApp()); } -final _localNotificationsPlugin = FlutterLocalNotificationsPlugin(); - -Future _setupNotifications() async { - const InitializationSettings settings = InitializationSettings( - android: AndroidInitializationSettings('@mipmap/ic_launcher'), - iOS: DarwinInitializationSettings(), - ); - - await _localNotificationsPlugin.initialize(settings).then((_) { - debugPrint('Local Notifications setup success'); - }).catchError((Object error) { - debugPrint('Local Notifications setup error: $error'); - }); -} - class RampFlutterApp extends StatefulWidget { const RampFlutterApp({Key? key}) : super(key: key); @@ -40,6 +22,7 @@ class RampFlutterApp extends StatefulWidget { } class _RampFlutterAppState extends State { + final _messengerKey = GlobalKey(); final ramp = RampFlutter(); final Configuration _configuration = Configuration(); @@ -89,11 +72,11 @@ class _RampFlutterAppState extends State { String purchaseViewToken, String apiUrl, ) { - _showNotification("Ramp Network Notification", "onramp purchase created"); + _showEvent("onramp purchase created"); } void onSendCryptoRequested(SendCryptoPayload payload) { - _showNotification("Ramp Network Notification", "send crypto requested"); + _showEvent("send crypto requested"); ramp.sendCrypto("123"); } @@ -102,16 +85,24 @@ class _RampFlutterAppState extends State { String saleViewToken, String apiUrl, ) { - _showNotification("Ramp Network Notification", "offramp sale created"); + _showEvent("offramp sale created"); } void onRampClosed() { - _showNotification("Ramp Network Notification", "ramp closed"); + _showEvent("ramp closed"); + } + + void _showEvent(String message) { + debugPrint('Ramp example: $message'); + _messengerKey.currentState + ?..hideCurrentSnackBar() + ..showSnackBar(SnackBar(content: Text(message))); } @override Widget build(BuildContext context) { return MaterialApp( + scaffoldMessengerKey: _messengerKey, home: Builder( builder: (context) => Scaffold( appBar: AppBar( @@ -290,16 +281,4 @@ class _RampFlutterAppState extends State { controller: TextEditingController(text: defaultValue), ); } - - Future _showNotification(String title, String message) async { - const AndroidNotificationDetails android = - AndroidNotificationDetails("channelId", "channelName"); - const NotificationDetails details = NotificationDetails(android: android); - await _localNotificationsPlugin.show( - 1, - title, - message, - details, - ); - } } diff --git a/example/pubspec.lock b/example/pubspec.lock index 275c9f4..dfcb6c6 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,14 +1,6 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - args: - dependency: transitive - description: - name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" - url: "https://pub.dev" - source: hosted - version: "2.5.0" characters: dependency: transitive description: @@ -17,14 +9,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - clock: - dependency: transitive - description: - name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" - source: hosted - version: "1.1.1" collection: dependency: transitive description: @@ -41,22 +25,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" - dbus: - dependency: transitive - description: - name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" - url: "https://pub.dev" - source: hosted - version: "0.7.10" - ffi: - dependency: transitive - description: - name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" - url: "https://pub.dev" - source: hosted - version: "2.1.2" flutter: dependency: "direct main" description: flutter @@ -70,30 +38,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" - flutter_local_notifications: - dependency: "direct main" - description: - name: flutter_local_notifications - sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00" - url: "https://pub.dev" - source: hosted - version: "16.3.3" - flutter_local_notifications_linux: - dependency: transitive - description: - name: flutter_local_notifications_linux - sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af - url: "https://pub.dev" - source: hosted - version: "4.0.1" - flutter_local_notifications_platform_interface: - dependency: transitive - description: - name: flutter_local_notifications_platform_interface - sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" - url: "https://pub.dev" - source: hosted - version: "7.2.0" flutter_platform_widgets: dependency: "direct main" description: @@ -139,14 +83,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 - url: "https://pub.dev" - source: hosted - version: "6.0.2" plugin_platform_interface: dependency: transitive description: @@ -167,14 +103,6 @@ packages: description: flutter source: sdk version: "0.0.0" - timezone: - dependency: transitive - description: - name: timezone - sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" - url: "https://pub.dev" - source: hosted - version: "0.9.4" url_launcher: dependency: transitive description: @@ -287,22 +215,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.26.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d - url: "https://pub.dev" - source: hosted - version: "1.0.4" - xml: - dependency: transitive - description: - name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.dev" - source: hosted - version: "6.5.0" sdks: dart: ">=3.12.0 <4.0.0" flutter: ">=3.44.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 07353dd..ef0afb2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -13,7 +13,6 @@ dependencies: path: ../ cupertino_icons: ^1.0.2 flutter_platform_widgets: ^6.0.2 - flutter_local_notifications: ^16.3.0 dev_dependencies: flutter_lints: ^2.0.0 From 203b0d5aac789c02664765486c157708a8bc5430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 12:25:28 +0200 Subject: [PATCH 06/56] pass through any js events --- CHANGELOG.md | 5 +- README.md | 12 +-- example/lib/main.dart | 51 ++++------- lib/internal/ramp_webview_controller.dart | 57 +++--------- lib/offramp_sale.dart | 60 ------------ lib/onramp_purchase.dart | 64 ------------- lib/ramp_flutter.dart | 35 ++++--- lib/send_crypto_payload.dart | 34 ------- test/ramp_webview_test.dart | 107 +++++----------------- 9 files changed, 79 insertions(+), 346 deletions(-) delete mode 100644 lib/offramp_sale.dart delete mode 100644 lib/onramp_purchase.dart delete mode 100644 lib/send_crypto_payload.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 7306dfc..dafe4f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,8 @@ `variant`. * Default base URL is now `https://app.rampnetwork.com`. * Raise minimum platforms to Android 7.0 (API 24) and iOS 13. -* Fix numeric event fields crashing on whole-number JSON values; - `OnrampPurchase.fiatValue` is now `double?`. -* Remove unused `SendCryptoPayload.toMap` and `SendCryptoAssetInfo.toMap`. +* Breaking: expose a single `onWidgetEvent` callback for all widget JS events; + remove specialized purchase/sale/send-crypto/close callbacks and related DTOs. ## 4.0.1 diff --git a/README.md b/README.md index ac06e7a..35fe272 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,12 @@ dependencies: ```dart final ramp = RampFlutter(); -ramp.onOnrampPurchaseCreated = (purchase, token, apiUrl) {}; -ramp.onOfframpSaleCreated = (sale, token, apiUrl) {}; -ramp.onSendCryptoRequested = (payload) { - // Send crypto, then: - ramp.sendCrypto(txHash); +ramp.onWidgetEvent = (event) { + // Every widget JS event, e.g. PURCHASE_CREATED, SEND_CRYPTO, CLOSE, ... + if (event['type'] == 'SEND_CRYPTO') { + ramp.sendCrypto(txHash); + } }; -ramp.onRampClosed = () {}; final configuration = Configuration() ..hostApiKey = 'YOUR_API_KEY' @@ -46,6 +45,7 @@ await ramp.showRamp(context, configuration); `showRamp` uses Flutter's [showModalBottomSheet](https://api.flutter.dev/flutter/material/showModalBottomSheet.html). The host app must provide a `MaterialApp` (or equivalent `MaterialLocalizations`) above the `BuildContext` you pass in. Swipe down from the drag handle to dismiss. +`CLOSE` / `WIDGET_CLOSE` also dismiss the sheet. For more configuration parameters see [Ramp Network Flutter documentation](https://docs.ramp.network/mobile/flutter-sdk/). diff --git a/example/lib/main.dart b/example/lib/main.dart index 905f061..270c188 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,11 +1,10 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:ramp_flutter/configuration.dart'; -import 'package:ramp_flutter/offramp_sale.dart'; -import 'package:ramp_flutter/onramp_purchase.dart'; import 'package:ramp_flutter/ramp_flutter.dart'; -import 'package:ramp_flutter/send_crypto_payload.dart'; import 'secrets.dart'; @@ -46,10 +45,7 @@ class _RampFlutterAppState extends State { _configuration.deepLinkScheme = "rampflutterdemo"; _applyEnvironment(_selectedEnvironment); - ramp.onOnrampPurchaseCreated = onOnrampPurchaseCreated; - ramp.onSendCryptoRequested = onSendCryptoRequested; - ramp.onOfframpSaleCreated = onOfframpSaleCreated; - ramp.onRampClosed = onRampClosed; + ramp.onWidgetEvent = onWidgetEvent; super.initState(); } @@ -67,36 +63,21 @@ class _RampFlutterAppState extends State { id == 0 ? ExampleSecrets.hostApiKeyInternal : null; } - void onOnrampPurchaseCreated( - OnrampPurchase purchase, - String purchaseViewToken, - String apiUrl, - ) { - _showEvent("onramp purchase created"); - } - - void onSendCryptoRequested(SendCryptoPayload payload) { - _showEvent("send crypto requested"); - ramp.sendCrypto("123"); - } - - void onOfframpSaleCreated( - OfframpSale sale, - String saleViewToken, - String apiUrl, - ) { - _showEvent("offramp sale created"); - } - - void onRampClosed() { - _showEvent("ramp closed"); - } - - void _showEvent(String message) { - debugPrint('Ramp example: $message'); + void onWidgetEvent(Map event) { + final encoded = const JsonEncoder.withIndent(' ').convert(event); + debugPrint('Ramp example event:\n$encoded'); _messengerKey.currentState ?..hideCurrentSnackBar() - ..showSnackBar(SnackBar(content: Text(message))); + ..showSnackBar( + SnackBar( + content: Text(encoded), + duration: const Duration(seconds: 4), + ), + ); + + if (event['type'] == 'SEND_CRYPTO') { + ramp.sendCrypto('123'); + } } @override diff --git a/lib/internal/ramp_webview_controller.dart b/lib/internal/ramp_webview_controller.dart index 7df55dc..87d3da7 100644 --- a/lib/internal/ramp_webview_controller.dart +++ b/lib/internal/ramp_webview_controller.dart @@ -7,11 +7,7 @@ import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter_android/webview_flutter_android.dart'; import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; -import '../offramp_sale.dart'; -import '../onramp_purchase.dart'; -import '../send_crypto_payload.dart'; - -/// Owns the widget WebView and forwards Ramp Instant JS events to Dart callbacks. +/// Owns the widget WebView and forwards Ramp Instant JS events to Dart. class RampWebViewController { RampWebViewController(this._widgetUrl); @@ -20,10 +16,8 @@ class RampWebViewController { final Uri _widgetUrl; WebViewController? _webView; - Function(OnrampPurchase, String, String)? onOnrampPurchaseCreated; - Function(OfframpSale, String, String)? onOfframpSaleCreated; - Function(SendCryptoPayload)? onSendCryptoRequested; - Function()? onClosed; + /// Called for every widget JS message. + Function(Map event)? onWidgetEvent; WebViewController get webViewController => _webView ??= _createWebViewController(); @@ -113,7 +107,6 @@ class RampWebViewController { if (uri.scheme == 'about') return true; if (uri.host.isEmpty) return true; if (uri.host == _widgetUrl.host) return true; - // Allow same-site Ramp hosts during widget redirects. return _trustedRampHost.hasMatch(uri.host); } @@ -134,7 +127,6 @@ class RampWebViewController { } } - /// Extracts a browser URL from an Android `intent://` URI when present. static Uri? _intentFallbackUrl(Uri intentUri) { final browserFallback = intentUri.queryParameters['browser_fallback_url']; if (browserFallback != null && browserFallback.isNotEmpty) { @@ -172,41 +164,20 @@ class RampWebViewController { try { event = jsonDecode(message); } on FormatException { + onWidgetEvent?.call({ + 'type': 'RAW', + 'payload': message, + }); return; } - if (event is! Map) return; - final eventMap = Map.from(event); - - final payload = eventMap['payload']; - switch (eventMap['type']) { - case 'PURCHASE_CREATED': - if (payload is! Map) return; - final payloadMap = Map.from(payload); - onOnrampPurchaseCreated?.call( - OnrampPurchase.fromArguments(payloadMap['purchase']), - payloadMap['purchaseViewToken'] as String? ?? '', - payloadMap['apiUrl'] as String? ?? '', - ); - break; - case 'OFFRAMP_SALE_CREATED': - if (payload is! Map) return; - final payloadMap = Map.from(payload); - onOfframpSaleCreated?.call( - OfframpSale.fromArguments(payloadMap['sale']), - payloadMap['saleViewToken'] as String? ?? '', - payloadMap['apiUrl'] as String? ?? '', - ); - break; - case 'SEND_CRYPTO': - onSendCryptoRequested?.call( - SendCryptoPayload.fromArguments(payload), - ); - break; - case 'CLOSE': - case 'WIDGET_CLOSE': - onClosed?.call(); - break; + if (event is! Map) { + onWidgetEvent?.call({ + 'type': 'RAW', + 'payload': event, + }); + return; } + onWidgetEvent?.call(Map.from(event)); } } diff --git a/lib/offramp_sale.dart b/lib/offramp_sale.dart deleted file mode 100644 index 2fab5f2..0000000 --- a/lib/offramp_sale.dart +++ /dev/null @@ -1,60 +0,0 @@ -class OfframpSale { - String? createdAt; - OfframpCrypto? crypto; - OfframpFiat? fiat; - String? id; - - static OfframpSale fromArguments(dynamic arguments) { - OfframpSale sale = OfframpSale(); - sale.createdAt = arguments["createdAt"]; - sale.crypto = OfframpCrypto.fromArguments(arguments['crypto']); - sale.fiat = OfframpFiat.fromArguments(arguments['fiat']); - sale.id = arguments["id"]; - return sale; - } -} - -class OfframpCrypto { - String? amount; - OfframpAssetInfo? assetInfo; - - static OfframpCrypto fromArguments(dynamic arguments) { - OfframpCrypto crypto = OfframpCrypto(); - if (arguments == null) return crypto; - crypto.amount = arguments["amount"]; - crypto.assetInfo = OfframpAssetInfo.fromArguments(arguments["assetInfo"]); - return crypto; - } -} - -class OfframpAssetInfo { - String? chain; - int? decimals; - String? name; - String? symbol; - String? type; - - static OfframpAssetInfo fromArguments(dynamic arguments) { - OfframpAssetInfo assetInfo = OfframpAssetInfo(); - if (arguments == null) return assetInfo; - assetInfo.chain = arguments["chain"]; - assetInfo.decimals = arguments["decimals"]; - assetInfo.name = arguments["name"]; - assetInfo.symbol = arguments["symbol"]; - assetInfo.type = arguments["type"]; - return assetInfo; - } -} - -class OfframpFiat { - double? amount; - String? currencySymbol; - - static OfframpFiat fromArguments(dynamic arguments) { - OfframpFiat fiat = OfframpFiat(); - if (arguments == null) return fiat; - fiat.amount = (arguments["amount"] as num?)?.toDouble(); - fiat.currencySymbol = arguments["currencySymbol"]; - return fiat; - } -} diff --git a/lib/onramp_purchase.dart b/lib/onramp_purchase.dart deleted file mode 100644 index a90e101..0000000 --- a/lib/onramp_purchase.dart +++ /dev/null @@ -1,64 +0,0 @@ -class OnrampPurchase { - String? id; - String? endTime; - PurchaseAssetInfo? asset; - String? receiverAddress; - String? cryptoAmount; - String? fiatCurrency; - double? fiatValue; - double? assetExchangeRate; - double? baseRampFee; - double? networkFee; - double? appliedFee; - String? paymentMethodType; - String? finalTxHash; - String? createdAt; - String? updatedAt; - String? status; - String? escrowAddress; - String? escrowDetailsHash; - - static OnrampPurchase fromArguments(dynamic arguments) { - OnrampPurchase purchase = OnrampPurchase(); - purchase.id = arguments["id"]; - purchase.endTime = arguments["endTime"]; - purchase.asset = PurchaseAssetInfo.fromArguments(arguments["asset"]); - purchase.receiverAddress = arguments["receiverAddress"]; - purchase.cryptoAmount = arguments["cryptoAmount"]; - purchase.fiatCurrency = arguments["fiatCurrency"]; - purchase.fiatValue = _toDouble(arguments["fiatValue"]); - purchase.assetExchangeRate = _toDouble(arguments["assetExchangeRate"]); - purchase.baseRampFee = _toDouble(arguments["baseRampFee"]); - purchase.networkFee = _toDouble(arguments["networkFee"]); - purchase.appliedFee = _toDouble(arguments["appliedFee"]); - purchase.paymentMethodType = arguments["paymentMethodType"]; - purchase.finalTxHash = arguments["finalTxHash"]; - purchase.createdAt = arguments["createdAt"]; - purchase.updatedAt = arguments["updatedAt"]; - purchase.status = arguments["status"]; - purchase.escrowAddress = arguments["escrowAddress"]; - purchase.escrowDetailsHash = arguments["escrowDetailsHash"]; - return purchase; - } -} - -double? _toDouble(dynamic value) => (value as num?)?.toDouble(); - -class PurchaseAssetInfo { - String? address; - int? decimals; - String? name; - String? symbol; - String? type; - - static PurchaseAssetInfo fromArguments(dynamic arguments) { - PurchaseAssetInfo assetInfo = PurchaseAssetInfo(); - if (arguments == null) return assetInfo; - assetInfo.address = arguments["address"]; - assetInfo.decimals = arguments["decimals"]; - assetInfo.name = arguments["name"]; - assetInfo.symbol = arguments["symbol"]; - assetInfo.type = arguments["type"]; - return assetInfo; - } -} diff --git a/lib/ramp_flutter.dart b/lib/ramp_flutter.dart index 1e19f40..3bf4b4a 100644 --- a/lib/ramp_flutter.dart +++ b/lib/ramp_flutter.dart @@ -2,33 +2,39 @@ import 'package:flutter/material.dart'; import 'package:ramp_flutter/configuration.dart'; import 'package:ramp_flutter/internal/ramp_webview_controller.dart'; import 'package:ramp_flutter/internal/ramp_webview_page.dart'; -import 'package:ramp_flutter/offramp_sale.dart'; -import 'package:ramp_flutter/onramp_purchase.dart'; -import 'package:ramp_flutter/send_crypto_payload.dart'; /// Flutter API for presenting the Ramp Network widget. class RampFlutter { RampWebViewController? _activeController; - Function(OnrampPurchase, String, String)? onOnrampPurchaseCreated; - Function(SendCryptoPayload payload)? onSendCryptoRequested; - Function(OfframpSale, String, String)? onOfframpSaleCreated; - Function()? onRampClosed; + /// Fires for every widget JS event. + Function(Map event)? onWidgetEvent; /// Builds the widget URL from [configuration] and presents it in a /// [showModalBottomSheet]. /// /// Requires a [MaterialApp] (or other ancestor that provides /// [MaterialLocalizations]) above [context]. + /// + /// `CLOSE` / `WIDGET_CLOSE` events dismiss the sheet automatically. Future showRamp( BuildContext context, Configuration configuration, ) async { final widgetUrl = configuration.buildWidgetUrl(); - final controller = RampWebViewController(widgetUrl) - ..onOnrampPurchaseCreated = onOnrampPurchaseCreated - ..onOfframpSaleCreated = onOfframpSaleCreated - ..onSendCryptoRequested = onSendCryptoRequested; + late final RampWebViewController controller; + + controller = RampWebViewController(widgetUrl) + ..onWidgetEvent = (event) { + onWidgetEvent?.call(event); + final type = event['type']; + if (type == 'CLOSE' || type == 'WIDGET_CLOSE') { + final navigator = Navigator.of(context, rootNavigator: true); + if (navigator.canPop()) { + navigator.pop(); + } + } + }; _activeController = controller; @@ -40,12 +46,6 @@ class RampFlutter { isDismissible: true, useSafeArea: true, builder: (sheetContext) { - controller.onClosed = () { - if (Navigator.of(sheetContext).canPop()) { - Navigator.of(sheetContext).pop(); - } - }; - return SizedBox( height: MediaQuery.sizeOf(sheetContext).height * 0.92, child: Column( @@ -73,7 +73,6 @@ class RampFlutter { if (identical(_activeController, controller)) { _activeController = null; } - onRampClosed?.call(); } /// Completes an off-ramp send-crypto request with an optional [transactionHash]. diff --git a/lib/send_crypto_payload.dart b/lib/send_crypto_payload.dart deleted file mode 100644 index 1ba73b1..0000000 --- a/lib/send_crypto_payload.dart +++ /dev/null @@ -1,34 +0,0 @@ -class SendCryptoPayload { - String? address; - String? amount; - SendCryptoAssetInfo? assetInfo; - - static SendCryptoPayload fromArguments(dynamic arguments) { - SendCryptoPayload payload = SendCryptoPayload(); - if (arguments == null) return payload; - payload.address = arguments["address"]; - payload.amount = arguments["amount"]; - payload.assetInfo = - SendCryptoAssetInfo.fromArguments(arguments["assetInfo"]); - return payload; - } -} - -class SendCryptoAssetInfo { - String? chain; - int? decimals; - String? name; - String? symbol; - String? type; - - static SendCryptoAssetInfo fromArguments(dynamic arguments) { - SendCryptoAssetInfo payload = SendCryptoAssetInfo(); - if (arguments == null) return payload; - payload.chain = arguments["chain"]; - payload.decimals = arguments["decimals"]; - payload.name = arguments["name"]; - payload.symbol = arguments["symbol"]; - payload.type = arguments["type"]; - return payload; - } -} diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index 9a1341a..7b44bc3 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -3,9 +3,6 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:ramp_flutter/configuration.dart'; import 'package:ramp_flutter/internal/ramp_webview_controller.dart'; -import 'package:ramp_flutter/offramp_sale.dart'; -import 'package:ramp_flutter/onramp_purchase.dart'; -import 'package:ramp_flutter/send_crypto_payload.dart'; void main() { group('Configuration.buildWidgetUrl', () { @@ -61,94 +58,38 @@ void main() { group('RampWebViewController events', () { final widgetUrl = Uri.parse('https://app.rampnetwork.com/'); - test('forwards purchase events with numeric coercion', () { - OnrampPurchase? purchase; - String? token; - String? apiUrl; + test('forwards every widget event on onWidgetEvent', () { + final events = >[]; final controller = RampWebViewController(widgetUrl) - ..onOnrampPurchaseCreated = (p, t, url) { - purchase = p; - token = t; - apiUrl = url; - }; - - controller.handleJavaScriptMessage(jsonEncode({ - 'type': 'PURCHASE_CREATED', - 'payload': { - 'purchase': { - 'id': 'purchase-id', - 'asset': {'decimals': 18, 'name': 'Ether', 'symbol': 'ETH'}, - 'fiatValue': 100.5, - 'assetExchangeRate': 2, - }, - 'purchaseViewToken': 'view-token', - 'apiUrl': 'https://api.ramp.network', - }, - })); - - expect(purchase?.id, 'purchase-id'); - expect(token, 'view-token'); - expect(apiUrl, 'https://api.ramp.network'); - expect(purchase?.fiatValue, 100.5); - expect(purchase?.assetExchangeRate, 2.0); - }); - - test('forwards sale events with whole-number fiat amounts', () { - OfframpSale? sale; - final controller = RampWebViewController(widgetUrl) - ..onOfframpSaleCreated = (s, token, url) => sale = s; - - controller.handleJavaScriptMessage(jsonEncode({ - 'type': 'OFFRAMP_SALE_CREATED', - 'payload': { - 'sale': { - 'id': 'sale-id', - 'crypto': { - 'amount': '1000000000000000000', - 'assetInfo': { - 'chain': 'ETH', - 'decimals': 18, - 'name': 'Ether', - 'symbol': 'ETH', - }, - }, - 'fiat': {'amount': 100, 'currencySymbol': 'EUR'}, - }, - 'saleViewToken': 'view-token', - 'apiUrl': 'https://api.ramp.network', - }, - })); - - expect(sale?.id, 'sale-id'); - expect(sale?.fiat?.amount, 100.0); - expect(sale?.crypto?.assetInfo?.chain, 'ETH'); - }); - - test('forwards send crypto requests and both close events', () { - SendCryptoPayload? payload; - var closedCount = 0; - final controller = RampWebViewController(widgetUrl) - ..onSendCryptoRequested = (p) { - payload = p; - } - ..onClosed = () => closedCount++; + ..onWidgetEvent = events.add; controller.handleJavaScriptMessage('not json'); controller.handleJavaScriptMessage('42'); controller.handleJavaScriptMessage(jsonEncode({ - 'type': 'SEND_CRYPTO', - 'payload': { - 'address': '0xabc', - 'amount': '1', - 'assetInfo': {'chain': 'ETH'}, - }, + 'type': 'WIDGET_CONFIG_DONE', + 'payload': {'ok': true}, + })); + controller.handleJavaScriptMessage(jsonEncode({ + 'type': 'PURCHASE_CREATED', + 'payload': {'purchase': {'id': 'purchase-id'}}, })); controller.handleJavaScriptMessage(jsonEncode({'type': 'CLOSE'})); - controller.handleJavaScriptMessage(jsonEncode({'type': 'WIDGET_CLOSE'})); - expect(payload?.address, '0xabc'); - expect(payload?.assetInfo?.chain, 'ETH'); - expect(closedCount, 2); + expect(events, [ + {'type': 'RAW', 'payload': 'not json'}, + {'type': 'RAW', 'payload': 42}, + { + 'type': 'WIDGET_CONFIG_DONE', + 'payload': {'ok': true}, + }, + { + 'type': 'PURCHASE_CREATED', + 'payload': { + 'purchase': {'id': 'purchase-id'}, + }, + }, + {'type': 'CLOSE'}, + ]); }); }); } From cc588f9fdff7dcc59c380d749d7fdc421410cc88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 12:30:06 +0200 Subject: [PATCH 07/56] move presentation responsibility to sdk integrator, sdk provides view only --- CHANGELOG.md | 4 +- README.md | 40 ++++++---- example/README.md | 3 +- example/lib/main.dart | 61 +++++++++++++-- lib/internal/ramp_webview_controller.dart | 2 +- lib/ramp_flutter.dart | 90 ++++++----------------- 6 files changed, 106 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dafe4f0..0ff4e14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,8 @@ * Rewrite the SDK to load the Ramp widget in a Flutter WebView instead of the native iOS/Android Ramp SDKs. Published as a Dart Flutter package (no Android/iOS plugin shells). -* Breaking: `showRamp` now requires a `BuildContext` and pushes a fullscreen - route (`showRamp(context, configuration)`). +* Breaking: SDK no longer presents UI. Create `RampFlutter(configuration)`, + embed `ramp.view` in your own route/sheet, and call `dispose` when done. * Add `Configuration.offrampAsset` and build the widget URL in Dart (`Configuration.buildWidgetUrl`), including `sdkType` / `sdkVersion` / `variant`. diff --git a/README.md b/README.md index 35fe272..312a202 100644 --- a/README.md +++ b/README.md @@ -24,29 +24,41 @@ dependencies: ### Usage +The SDK provides the Ramp WebView; your app owns presentation (route, bottom +sheet, dialog, etc.) and must call `dispose` when it is dismissed. + ```dart -final ramp = RampFlutter(); -ramp.onWidgetEvent = (event) { - // Every widget JS event, e.g. PURCHASE_CREATED, SEND_CRYPTO, CLOSE, ... - if (event['type'] == 'SEND_CRYPTO') { - ramp.sendCrypto(txHash); - } -}; +final ramp = RampFlutter(configuration) + ..onWidgetEvent = (event) { + // Every widget JS event, e.g. PURCHASE_CREATED, SEND_CRYPTO, CLOSE, ... + if (event['type'] == 'SEND_CRYPTO') { + ramp.sendCrypto(txHash); + } + if (event['type'] == 'CLOSE' || event['type'] == 'WIDGET_CLOSE') { + Navigator.of(context).pop(); + } + }; + +await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => SizedBox( + height: MediaQuery.sizeOf(context).height * 0.92, + child: ramp.view, + ), +); + +ramp.dispose(); +``` +```dart final configuration = Configuration() ..hostApiKey = 'YOUR_API_KEY' ..hostAppName = 'My App' ..hostLogoUrl = 'https://example.com/logo.png' ..enabledFlows = ['ONRAMP', 'OFFRAMP']; - -await ramp.showRamp(context, configuration); ``` -`showRamp` uses Flutter's [showModalBottomSheet](https://api.flutter.dev/flutter/material/showModalBottomSheet.html). -The host app must provide a `MaterialApp` (or equivalent `MaterialLocalizations`) -above the `BuildContext` you pass in. Swipe down from the drag handle to dismiss. -`CLOSE` / `WIDGET_CLOSE` also dismiss the sheet. - For more configuration parameters see [Ramp Network Flutter documentation](https://docs.ramp.network/mobile/flutter-sdk/). diff --git a/example/README.md b/example/README.md index a3aaef8..cf12f1e 100644 --- a/example/README.md +++ b/example/README.md @@ -2,7 +2,8 @@ Demonstrates the Ramp Network Flutter WebView SDK (`ramp_flutter` 5.0.0). -Use **Show Ramp** to present the widget via `ramp.showRamp(context, configuration)`. +Use **Show Ramp** to present `ramp.view` in a modal bottom sheet (presentation +is owned by the example, not the SDK). ## Local secrets diff --git a/example/lib/main.dart b/example/lib/main.dart index 270c188..39ecab7 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -22,8 +22,8 @@ class RampFlutterApp extends StatefulWidget { class _RampFlutterAppState extends State { final _messengerKey = GlobalKey(); - final ramp = RampFlutter(); final Configuration _configuration = Configuration(); + RampFlutter? _ramp; final List _predefinedEnvironments = [ "https://app.dev.ramp-network.org", @@ -45,8 +45,6 @@ class _RampFlutterAppState extends State { _configuration.deepLinkScheme = "rampflutterdemo"; _applyEnvironment(_selectedEnvironment); - ramp.onWidgetEvent = onWidgetEvent; - super.initState(); } @@ -63,7 +61,7 @@ class _RampFlutterAppState extends State { id == 0 ? ExampleSecrets.hostApiKeyInternal : null; } - void onWidgetEvent(Map event) { + void _onWidgetEvent(BuildContext context, Map event) { final encoded = const JsonEncoder.withIndent(' ').convert(event); debugPrint('Ramp example event:\n$encoded'); _messengerKey.currentState @@ -75,8 +73,57 @@ class _RampFlutterAppState extends State { ), ); - if (event['type'] == 'SEND_CRYPTO') { - ramp.sendCrypto('123'); + final type = event['type']; + if (type == 'SEND_CRYPTO') { + _ramp?.sendCrypto('123'); + } + if (type == 'CLOSE' || type == 'WIDGET_CLOSE') { + final navigator = Navigator.of(context, rootNavigator: true); + if (navigator.canPop()) { + navigator.pop(); + } + } + } + + Future _showRamp(BuildContext context) async { + final ramp = RampFlutter(_configuration) + ..onWidgetEvent = (event) => _onWidgetEvent(context, event); + _ramp = ramp; + + await showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + enableDrag: true, + isDismissible: true, + useSafeArea: true, + builder: (sheetContext) { + return SizedBox( + height: MediaQuery.sizeOf(sheetContext).height * 0.92, + child: Column( + children: [ + const SizedBox( + height: 28, + child: Center( + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.all(Radius.circular(2)), + ), + child: SizedBox(width: 36, height: 4), + ), + ), + ), + Expanded(child: ramp.view), + ], + ), + ); + }, + ); + + ramp.dispose(); + if (identical(_ramp, ramp)) { + _ramp = null; } } @@ -233,7 +280,7 @@ class _RampFlutterAppState extends State { Widget _showRampButton(BuildContext context) { return PlatformTextButton( - onPressed: () => ramp.showRamp(context, _configuration), + onPressed: () => _showRamp(context), child: PlatformText("Show Ramp"), ); } diff --git a/lib/internal/ramp_webview_controller.dart b/lib/internal/ramp_webview_controller.dart index 87d3da7..b19cef5 100644 --- a/lib/internal/ramp_webview_controller.dart +++ b/lib/internal/ramp_webview_controller.dart @@ -150,7 +150,7 @@ class RampWebViewController { ); } - /// Stops the page and releases capture. Call when the route is dismissed. + /// Stops the page and releases capture. Call when the host dismisses the view. void dispose() { _webView ?..removeJavaScriptChannel(_channelName) diff --git a/lib/ramp_flutter.dart b/lib/ramp_flutter.dart index 3bf4b4a..4745339 100644 --- a/lib/ramp_flutter.dart +++ b/lib/ramp_flutter.dart @@ -1,82 +1,34 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:ramp_flutter/configuration.dart'; import 'package:ramp_flutter/internal/ramp_webview_controller.dart'; import 'package:ramp_flutter/internal/ramp_webview_page.dart'; -/// Flutter API for presenting the Ramp Network widget. +/// Flutter API for embedding the Ramp Network widget. +/// +/// The SDK provides the widget [view]; the host app owns presentation +/// (route, bottom sheet, dialog, etc.) and must call [dispose] when done. class RampFlutter { - RampWebViewController? _activeController; - - /// Fires for every widget JS event. - Function(Map event)? onWidgetEvent; - - /// Builds the widget URL from [configuration] and presents it in a - /// [showModalBottomSheet]. - /// - /// Requires a [MaterialApp] (or other ancestor that provides - /// [MaterialLocalizations]) above [context]. - /// - /// `CLOSE` / `WIDGET_CLOSE` events dismiss the sheet automatically. - Future showRamp( - BuildContext context, - Configuration configuration, - ) async { - final widgetUrl = configuration.buildWidgetUrl(); - late final RampWebViewController controller; + RampFlutter(Configuration configuration) + : _controller = + RampWebViewController(configuration.buildWidgetUrl()) { + _controller.onWidgetEvent = (event) => onWidgetEvent?.call(event); + } - controller = RampWebViewController(widgetUrl) - ..onWidgetEvent = (event) { - onWidgetEvent?.call(event); - final type = event['type']; - if (type == 'CLOSE' || type == 'WIDGET_CLOSE') { - final navigator = Navigator.of(context, rootNavigator: true); - if (navigator.canPop()) { - navigator.pop(); - } - } - }; + final RampWebViewController _controller; - _activeController = controller; + /// Fires for every widget JS event. + void Function(Map event)? onWidgetEvent; - await showModalBottomSheet( - context: context, - useRootNavigator: true, - isScrollControlled: true, - enableDrag: true, - isDismissible: true, - useSafeArea: true, - builder: (sheetContext) { - return SizedBox( - height: MediaQuery.sizeOf(sheetContext).height * 0.92, - child: Column( - children: [ - const SizedBox( - height: 28, - child: Center( - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.all(Radius.circular(2)), - ), - child: SizedBox(width: 36, height: 4), - ), - ), - ), - Expanded(child: RampWebViewPage(controller: controller)), - ], - ), - ); - }, - ); + /// WebView that loads the Ramp widget. Embed this in your own UI. + Widget get view => RampWebViewPage(controller: _controller); - controller.dispose(); - if (identical(_activeController, controller)) { - _activeController = null; - } + /// Completes an off-ramp send-crypto request with an optional [transactionHash]. + Future sendCrypto(String? transactionHash) { + return _controller.sendCrypto(transactionHash); } - /// Completes an off-ramp send-crypto request with an optional [transactionHash]. - Future sendCrypto(String? transactionHash) async { - await _activeController?.sendCrypto(transactionHash); + /// Releases the WebView. Call when your presentation is dismissed. + void dispose() { + _controller.dispose(); } } From a0e687e8de264b8d52e9599804f9758d9442523e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 12:34:58 +0200 Subject: [PATCH 08/56] show example toasts on top of widget --- example/lib/main.dart | 56 +++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 39ecab7..b99dee1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -24,6 +24,8 @@ class _RampFlutterAppState extends State { final _messengerKey = GlobalKey(); final Configuration _configuration = Configuration(); RampFlutter? _ramp; + /// Messenger for the open sheet so snackbars appear above the WebView. + GlobalKey? _sheetMessengerKey; final List _predefinedEnvironments = [ "https://app.dev.ramp-network.org", @@ -61,17 +63,24 @@ class _RampFlutterAppState extends State { id == 0 ? ExampleSecrets.hostApiKeyInternal : null; } - void _onWidgetEvent(BuildContext context, Map event) { - final encoded = const JsonEncoder.withIndent(' ').convert(event); - debugPrint('Ramp example event:\n$encoded'); - _messengerKey.currentState + void _showEventToast(String message) { + final messenger = + _sheetMessengerKey?.currentState ?? _messengerKey.currentState; + messenger ?..hideCurrentSnackBar() ..showSnackBar( SnackBar( - content: Text(encoded), + content: Text(message), duration: const Duration(seconds: 4), + behavior: SnackBarBehavior.floating, ), ); + } + + void _onWidgetEvent(BuildContext context, Map event) { + final encoded = const JsonEncoder.withIndent(' ').convert(event); + debugPrint('Ramp example event:\n$encoded'); + _showEventToast(encoded); final type = event['type']; if (type == 'SEND_CRYPTO') { @@ -88,7 +97,9 @@ class _RampFlutterAppState extends State { Future _showRamp(BuildContext context) async { final ramp = RampFlutter(_configuration) ..onWidgetEvent = (event) => _onWidgetEvent(context, event); + final sheetMessengerKey = GlobalKey(); _ramp = ramp; + _sheetMessengerKey = sheetMessengerKey; await showModalBottomSheet( context: context, @@ -100,22 +111,28 @@ class _RampFlutterAppState extends State { builder: (sheetContext) { return SizedBox( height: MediaQuery.sizeOf(sheetContext).height * 0.92, - child: Column( - children: [ - const SizedBox( - height: 28, - child: Center( - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.all(Radius.circular(2)), + child: ScaffoldMessenger( + key: sheetMessengerKey, + child: Scaffold( + backgroundColor: Colors.white, + body: Column( + children: [ + const SizedBox( + height: 28, + child: Center( + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.all(Radius.circular(2)), + ), + child: SizedBox(width: 36, height: 4), + ), ), - child: SizedBox(width: 36, height: 4), ), - ), + Expanded(child: ramp.view), + ], ), - Expanded(child: ramp.view), - ], + ), ), ); }, @@ -125,6 +142,9 @@ class _RampFlutterAppState extends State { if (identical(_ramp, ramp)) { _ramp = null; } + if (identical(_sheetMessengerKey, sheetMessengerKey)) { + _sheetMessengerKey = null; + } } @override From 2841f4efe6e5ed4a843b1ca2e2e2de0ad623e825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 12:45:20 +0200 Subject: [PATCH 09/56] fix js message parsing --- lib/internal/ramp_webview_controller.dart | 12 ++++-------- test/ramp_webview_test.dart | 13 ++++++++----- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/internal/ramp_webview_controller.dart b/lib/internal/ramp_webview_controller.dart index b19cef5..7a8f900 100644 --- a/lib/internal/ramp_webview_controller.dart +++ b/lib/internal/ramp_webview_controller.dart @@ -160,21 +160,17 @@ class RampWebViewController { @visibleForTesting void handleJavaScriptMessage(String message) { + // The widget posts to both Android (`RampInstantMobile` + JSON.stringify) + // and iOS (`webkit.messageHandlers` + JS object). Flutter's channel + // receives both; the object path arrives as a non-JSON Map-style string. + // Prefer valid JSON event maps and ignore the rest. final dynamic event; try { event = jsonDecode(message); } on FormatException { - onWidgetEvent?.call({ - 'type': 'RAW', - 'payload': message, - }); return; } if (event is! Map) { - onWidgetEvent?.call({ - 'type': 'RAW', - 'payload': event, - }); return; } onWidgetEvent?.call(Map.from(event)); diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index 7b44bc3..62a9e98 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -58,16 +58,20 @@ void main() { group('RampWebViewController events', () { final widgetUrl = Uri.parse('https://app.rampnetwork.com/'); - test('forwards every widget event on onWidgetEvent', () { + test('forwards JSON event maps and ignores non-JSON twins', () { final events = >[]; final controller = RampWebViewController(widgetUrl) ..onWidgetEvent = events.add; controller.handleJavaScriptMessage('not json'); controller.handleJavaScriptMessage('42'); + controller.handleJavaScriptMessage( + '{widgetInstanceId: abc, type: WIDGET_CONFIG_DONE, payload: null}', + ); controller.handleJavaScriptMessage(jsonEncode({ 'type': 'WIDGET_CONFIG_DONE', - 'payload': {'ok': true}, + 'payload': null, + 'widgetInstanceId': 'abc', })); controller.handleJavaScriptMessage(jsonEncode({ 'type': 'PURCHASE_CREATED', @@ -76,11 +80,10 @@ void main() { controller.handleJavaScriptMessage(jsonEncode({'type': 'CLOSE'})); expect(events, [ - {'type': 'RAW', 'payload': 'not json'}, - {'type': 'RAW', 'payload': 42}, { 'type': 'WIDGET_CONFIG_DONE', - 'payload': {'ok': true}, + 'payload': null, + 'widgetInstanceId': 'abc', }, { 'type': 'PURCHASE_CREATED', From 9ad3a31cf5e4e91dbba043d0f94419405d4c0c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 12:57:47 +0200 Subject: [PATCH 10/56] drop cococapods --- example/ios/Flutter/Debug.xcconfig | 1 - example/ios/Flutter/Release.xcconfig | 1 - example/ios/Podfile | 40 --------------- example/ios/Podfile.lock | 16 ------ example/ios/Runner.xcodeproj/project.pbxproj | 49 ------------------- .../contents.xcworkspacedata | 3 -- 6 files changed, 110 deletions(-) delete mode 100644 example/ios/Podfile delete mode 100644 example/ios/Podfile.lock diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index ec97fc6..592ceee 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig index c4855bf..592ceee 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile deleted file mode 100644 index 6be20bc..0000000 --- a/example/ios/Podfile +++ /dev/null @@ -1,40 +0,0 @@ -platform :ios, '13.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock deleted file mode 100644 index 587e089..0000000 --- a/example/ios/Podfile.lock +++ /dev/null @@ -1,16 +0,0 @@ -PODS: - - Flutter (1.0.0) - -DEPENDENCIES: - - Flutter (from `Flutter`) - -EXTERNAL SOURCES: - Flutter: - :path: Flutter - -SPEC CHECKSUMS: - Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - -PODFILE CHECKSUM: d2243213672c3c48aae53c36642ba411a6be7309 - -COCOAPODS: 1.17.0 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index aadab44..5e53361 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; - 93025B5D7BBBFC1A8516BDCD /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 089427D76D90C916BF2DEB27 /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -31,12 +30,9 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 089427D76D90C916BF2DEB27 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3464FAECD6AED0D34E8B0C89 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 5491CBB1735E80A1F7448520 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; @@ -48,7 +44,6 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B2968B4289079AC5A10ACE82 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -57,21 +52,12 @@ buildActionMask = 2147483647; files = ( 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, - 93025B5D7BBBFC1A8516BDCD /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 0ED26C7AA03001342DE76ABE /* Frameworks */ = { - isa = PBXGroup; - children = ( - 089427D76D90C916BF2DEB27 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -90,8 +76,6 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - AE0A181C66D88E3021D74FEF /* Pods */, - 0ED26C7AA03001342DE76ABE /* Frameworks */, ); sourceTree = ""; }; @@ -118,16 +102,6 @@ path = Runner; sourceTree = ""; }; - AE0A181C66D88E3021D74FEF /* Pods */ = { - isa = PBXGroup; - children = ( - 5491CBB1735E80A1F7448520 /* Pods-Runner.debug.xcconfig */, - 3464FAECD6AED0D34E8B0C89 /* Pods-Runner.release.xcconfig */, - B2968B4289079AC5A10ACE82 /* Pods-Runner.profile.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -135,7 +109,6 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 09BBB8AE4928621E4B8C77B3 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -207,28 +180,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 09BBB8AE4928621E4B8C77B3 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata index 21a3cc1..1d526a1 100644 --- a/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,7 +4,4 @@ - - From 10d96e0aff94e58dbea77345ad8563d82d30a874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 13:03:39 +0200 Subject: [PATCH 11/56] update dependencies --- CHANGELOG.md | 2 + README.md | 1 + example/lib/main.dart | 326 ++++++++++++++++++++++++++---------------- example/pubspec.lock | 36 ++--- example/pubspec.yaml | 6 +- pubspec.lock | 16 +-- pubspec.yaml | 14 +- 7 files changed, 229 insertions(+), 172 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ff4e14..d8f4c9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ `variant`. * Default base URL is now `https://app.rampnetwork.com`. * Raise minimum platforms to Android 7.0 (API 24) and iOS 13. +* Raise minimum Flutter to 3.44 / Dart 3.12; update `webview_flutter`, + `url_launcher`, and `flutter_lints`. * Breaking: expose a single `onWidgetEvent` callback for all widget JS events; remove specialized purchase/sale/send-crypto/close callbacks and related DTOs. diff --git a/README.md b/README.md index 312a202..cf8fefe 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ dependencies: - Native Ramp iOS/Android SDKs and empty Flutter plugin shells are **not** required (no CocoaPods `Ramp` pod, no JitPack `ramp-sdk-android`). Platform WebView support comes from `webview_flutter` / `url_launcher`. +- Requires Flutter 3.44+ / Dart 3.12+. ### Usage diff --git a/example/lib/main.dart b/example/lib/main.dart index b99dee1..64b4d7c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:ramp_flutter/configuration.dart'; import 'package:ramp_flutter/ramp_flutter.dart'; @@ -14,18 +13,17 @@ void main() { } class RampFlutterApp extends StatefulWidget { - const RampFlutterApp({Key? key}) : super(key: key); + const RampFlutterApp({super.key}); @override State createState() => _RampFlutterAppState(); } class _RampFlutterAppState extends State { - final _messengerKey = GlobalKey(); final Configuration _configuration = Configuration(); - RampFlutter? _ramp; - /// Messenger for the open sheet so snackbars appear above the WebView. - GlobalKey? _sheetMessengerKey; + final ValueNotifier> _debugEvents = + ValueNotifier>(const []); + var _nextDebugEventId = 0; final List _predefinedEnvironments = [ "https://app.dev.ramp-network.org", @@ -50,9 +48,15 @@ class _RampFlutterAppState extends State { super.initState(); } + @override + void dispose() { + _debugEvents.dispose(); + super.dispose(); + } + void _selectEnvironment(int id) { _applyEnvironment(id); - setState(() => {}); + setState(() {}); } void _applyEnvironment(int id) { @@ -63,43 +67,33 @@ class _RampFlutterAppState extends State { id == 0 ? ExampleSecrets.hostApiKeyInternal : null; } - void _showEventToast(String message) { - final messenger = - _sheetMessengerKey?.currentState ?? _messengerKey.currentState; - messenger - ?..hideCurrentSnackBar() - ..showSnackBar( - SnackBar( - content: Text(message), - duration: const Duration(seconds: 4), - behavior: SnackBarBehavior.floating, - ), - ); - } - - void _onWidgetEvent(BuildContext context, Map event) { + void _addDebugEvent(Map event) { final encoded = const JsonEncoder.withIndent(' ').convert(event); debugPrint('Ramp example event:\n$encoded'); - _showEventToast(encoded); + _debugEvents.value = [ + ..._debugEvents.value, + _DebugEvent(_nextDebugEventId++, encoded), + ]; + } - final type = event['type']; - if (type == 'SEND_CRYPTO') { - _ramp?.sendCrypto('123'); - } - if (type == 'CLOSE' || type == 'WIDGET_CLOSE') { - final navigator = Navigator.of(context, rootNavigator: true); - if (navigator.canPop()) { - navigator.pop(); - } - } + void _removeDebugEvent(int id) { + _debugEvents.value = + _debugEvents.value.where((e) => e.id != id).toList(growable: false); } Future _showRamp(BuildContext context) async { - final ramp = RampFlutter(_configuration) - ..onWidgetEvent = (event) => _onWidgetEvent(context, event); - final sheetMessengerKey = GlobalKey(); - _ramp = ramp; - _sheetMessengerKey = sheetMessengerKey; + final ramp = RampFlutter(_configuration); + + ramp.onWidgetEvent = (event) { + _addDebugEvent(event); + + final type = event['type']; + if (type == 'SEND_CRYPTO') { + ramp.sendCrypto('123'); + } + // Do not auto-pop on CLOSE/WIDGET_CLOSE — keep banners visible; + // dismiss the sheet manually. + }; await showModalBottomSheet( context: context, @@ -111,27 +105,38 @@ class _RampFlutterAppState extends State { builder: (sheetContext) { return SizedBox( height: MediaQuery.sizeOf(sheetContext).height * 0.92, - child: ScaffoldMessenger( - key: sheetMessengerKey, - child: Scaffold( - backgroundColor: Colors.white, - body: Column( - children: [ - const SizedBox( - height: 28, - child: Center( - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.all(Radius.circular(2)), + child: Material( + color: Colors.white, + child: Stack( + children: [ + Column( + children: [ + const SizedBox( + height: 28, + child: Center( + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.all(Radius.circular(2)), + ), + child: SizedBox(width: 36, height: 4), ), - child: SizedBox(width: 36, height: 4), ), ), + Expanded(child: ramp.view), + ], + ), + Positioned( + left: 8, + right: 8, + top: 36, + child: _DebugEventList( + eventsListenable: _debugEvents, + maxHeight: MediaQuery.sizeOf(sheetContext).height * 0.45, + onDismiss: _removeDebugEvent, ), - Expanded(child: ramp.view), - ], - ), + ), + ], ), ), ); @@ -139,28 +144,35 @@ class _RampFlutterAppState extends State { ); ramp.dispose(); - if (identical(_ramp, ramp)) { - _ramp = null; - } - if (identical(_sheetMessengerKey, sheetMessengerKey)) { - _sheetMessengerKey = null; - } } @override Widget build(BuildContext context) { return MaterialApp( - scaffoldMessengerKey: _messengerKey, home: Builder( builder: (context) => Scaffold( appBar: AppBar( title: const Text('Ramp Network Flutter'), ), - body: Padding( - padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), - child: ListView( - children: _formFields(context), - ), + body: Stack( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), + child: ListView( + children: _formFields(context), + ), + ), + Positioned( + left: 8, + right: 8, + top: 8, + child: _DebugEventList( + eventsListenable: _debugEvents, + maxHeight: MediaQuery.sizeOf(context).height * 0.5, + onDismiss: _removeDebugEvent, + ), + ), + ], ), ), ), @@ -172,7 +184,7 @@ class _RampFlutterAppState extends State { } Widget _appInfo() { - return PlatformText("App version: Flutter WebView"); + return const Text("App version: Flutter WebView"); } List _configurationForm() { @@ -182,7 +194,7 @@ class _RampFlutterAppState extends State { ["dev", "demo", "prod"], _selectEnvironment, ), - PlatformText( + Text( _predefinedEnvironments[_selectedEnvironment], style: const TextStyle( color: Color.fromRGBO(46, 190, 117, 1), @@ -245,88 +257,148 @@ class _RampFlutterAppState extends State { } Widget _enabledFlows() { - List flows = _configuration.enabledFlows ?? []; - PlatformSwitch onRamp = PlatformSwitch( - value: flows.contains("ONRAMP"), - onChanged: (enabled) { - if (enabled) { - flows.add("ONRAMP"); - } else { - flows.remove("ONRAMP"); - } - _configuration.enabledFlows = flows; - setState(() => {}); - }, - ); - PlatformSwitch offRamp = PlatformSwitch( - value: flows.contains("OFFRAMP"), - onChanged: (enabled) { - if (enabled) { - flows.add("OFFRAMP"); - } else { - flows.remove("OFFRAMP"); - } - _configuration.enabledFlows = flows; - setState(() => {}); - }, - ); - PlatformSwitch swap = PlatformSwitch( - value: flows.contains("SWAP"), - onChanged: (enabled) { - if (enabled) { - flows.add("SWAP"); - } else { - flows.remove("SWAP"); - } - _configuration.enabledFlows = flows; - setState(() => {}); - }, - ); + final flows = _configuration.enabledFlows ?? []; + + Widget flowSwitch(String name) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(name), + Switch( + value: flows.contains(name), + onChanged: (enabled) { + if (enabled) { + flows.add(name); + } else { + flows.remove(name); + } + _configuration.enabledFlows = flows; + setState(() {}); + }, + ), + ], + ); + } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - PlatformText("Enabled flows:"), - Row(children: [ - PlatformText("ONRAMP"), - onRamp, - PlatformText("OFFRAMP"), - offRamp, - PlatformText("SWAP"), - swap, - ]), + const Text("Enabled flows:"), + Row( + children: [ + flowSwitch("ONRAMP"), + flowSwitch("OFFRAMP"), + flowSwitch("SWAP"), + ], + ), ], ); } Widget _showRampButton(BuildContext context) { - return PlatformTextButton( + return TextButton( onPressed: () => _showRamp(context), - child: PlatformText("Show Ramp"), + child: const Text("Show Ramp"), ); } Row _segmentedControl( - String title, List options, void Function(int) itemSelected) { - List segments = options.asMap().entries.map((entry) { - return PlatformTextButton( + String title, + List options, + void Function(int) itemSelected, + ) { + final segments = options.asMap().entries.map((entry) { + return TextButton( onPressed: () => itemSelected(entry.key), - child: PlatformText(entry.value), + child: Text(entry.value), ); }).toList(); - List children = [PlatformText(title)]; - children.addAll(segments); - return Row(children: children); + return Row(children: [Text(title), ...segments]); } - PlatformTextField _textField( + TextField _textField( String placeholder, void Function(String) onChanged, String? defaultValue, ) { - return PlatformTextField( - hintText: placeholder, + return TextField( + decoration: InputDecoration(hintText: placeholder), onChanged: onChanged, controller: TextEditingController(text: defaultValue), ); } } + +class _DebugEvent { + _DebugEvent(this.id, this.body); + + final int id; + final String body; +} + +class _DebugEventList extends StatelessWidget { + const _DebugEventList({ + required this.eventsListenable, + required this.maxHeight, + required this.onDismiss, + }); + + final ValueNotifier> eventsListenable; + final double maxHeight; + final void Function(int id) onDismiss; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder>( + valueListenable: eventsListenable, + builder: (context, events, _) { + if (events.isEmpty) { + return const SizedBox.shrink(); + } + return ConstrainedBox( + constraints: BoxConstraints(maxHeight: maxHeight), + child: ListView.separated( + shrinkWrap: true, + itemCount: events.length, + separatorBuilder: (context, index) => const SizedBox(height: 6), + itemBuilder: (context, index) { + final event = events[index]; + return Material( + elevation: 3, + borderRadius: BorderRadius.circular(8), + color: const Color(0xFF323232), + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 8, 4, 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + event.body, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontFamily: 'Courier', + ), + ), + ), + IconButton( + visualDensity: VisualDensity.compact, + icon: const Icon( + Icons.close, + color: Colors.white70, + size: 18, + ), + onPressed: () => onDismiss(event.id), + ), + ], + ), + ), + ); + }, + ), + ); + }, + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index dfcb6c6..50f0b1c 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -17,14 +17,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" - source: hosted - version: "1.0.8" flutter: dependency: "direct main" description: flutter @@ -34,18 +26,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 - url: "https://pub.dev" - source: hosted - version: "2.0.3" - flutter_platform_widgets: - dependency: "direct main" - description: - name: flutter_platform_widgets - sha256: c483c0591d845d2adb84e341a1cfb746f1a8a7aff4c72a5957772446020601f4 + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -55,10 +39,10 @@ packages: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "6.1.0" material_color_utilities: dependency: transitive description: @@ -79,10 +63,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" plugin_platform_interface: dependency: transitive description: @@ -187,18 +171,18 @@ packages: dependency: transitive description: name: webview_flutter - sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736 + sha256: d53e1ccf5516f25017e3c9d44c39034db352d20fa34fe200674270242c2c5111 url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.14.1" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558" + sha256: a97db7a44f8e71af2f3971c45550a08cce1fb60059c1b8e534251e6cfb753490 url: "https://pub.dev" source: hosted - version: "3.16.9" + version: "4.13.0" webview_flutter_platform_interface: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ef0afb2..0af8c38 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,18 +4,16 @@ version: 1.0.0+1 publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: - sdk: '>=3.2.3 <4.0.0' + sdk: '>=3.12.0 <4.0.0' dependencies: flutter: sdk: flutter ramp_flutter: path: ../ - cupertino_icons: ^1.0.2 - flutter_platform_widgets: ^6.0.2 dev_dependencies: - flutter_lints: ^2.0.0 + flutter_lints: ^6.0.0 flutter: uses-material-design: true diff --git a/pubspec.lock b/pubspec.lock index d4ae7ba..a1c6d43 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -58,10 +58,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "6.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -100,10 +100,10 @@ packages: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "6.1.0" matcher: dependency: transitive description: @@ -289,18 +289,18 @@ packages: dependency: "direct main" description: name: webview_flutter - sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736 + sha256: d53e1ccf5516f25017e3c9d44c39034db352d20fa34fe200674270242c2c5111 url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.14.1" webview_flutter_android: dependency: "direct main" description: name: webview_flutter_android - sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558" + sha256: a97db7a44f8e71af2f3971c45550a08cce1fb60059c1b8e534251e6cfb753490 url: "https://pub.dev" source: hosted - version: "3.16.9" + version: "4.13.0" webview_flutter_platform_interface: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bf24696..77bf17c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,18 +4,18 @@ version: 5.0.0 homepage: https://ramp.network/ environment: - sdk: '>=3.2.3 <4.0.0' - flutter: '>=3.3.0' + sdk: '>=3.12.0 <4.0.0' + flutter: '>=3.44.0' dependencies: flutter: sdk: flutter - url_launcher: ^6.2.5 - webview_flutter: ^4.2.4 - webview_flutter_android: ^3.16.0 - webview_flutter_wkwebview: ^3.14.0 + url_launcher: ^6.3.2 + webview_flutter: ^4.14.1 + webview_flutter_android: ^4.13.0 + webview_flutter_wkwebview: ^3.26.0 dev_dependencies: - flutter_lints: ^2.0.0 + flutter_lints: ^6.0.0 flutter_test: sdk: flutter From 10b505beef5b957d5d82f536aaeac506434325e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 14:54:18 +0200 Subject: [PATCH 12/56] use forked webview to properly handle external links --- CHANGELOG.md | 3 ++ README.md | 31 ++++++++++++++++ example/pubspec.lock | 44 ++++++++++++---------- example/pubspec.yaml | 24 ++++++++++++ lib/internal/ramp_webview_controller.dart | 45 +++++++---------------- pubspec.lock | 38 ++++++++++--------- pubspec.yaml | 26 +++++++++++++ 7 files changed, 143 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8f4c9f..09ea98d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ `url_launcher`, and `flutter_lints`. * Breaking: expose a single `onWidgetEvent` callback for all widget JS events; remove specialized purchase/sale/send-crypto/close callbacks and related DTOs. +* Open `target=_blank` / `window.open` in the system browser via a forked + `webview_flutter` platform package (`flutter_packages` / + `webview-target-blank`). ## 4.0.1 diff --git a/README.md b/README.md index cf8fefe..4ec0582 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,37 @@ For more configuration parameters see ### Notes +- `target=_blank` / `window.open` use `NavigationDelegate.onCreateWindow` + via a fork of `webview_flutter` + ([mateusz-ramp/flutter_packages](https://github.com/mateusz-ramp/flutter_packages/tree/webview-target-blank) + branch `webview-target-blank`), matching native `createWebView` / + `onCreateWindow`. The SDK opens those URLs in the system browser. Host apps + (and `example/`) must declare matching `dependency_overrides`: + +```yaml +dependency_overrides: + webview_flutter: + git: + url: https://github.com/mateusz-ramp/flutter_packages.git + ref: webview-target-blank + path: packages/webview_flutter/webview_flutter + webview_flutter_android: + git: + url: https://github.com/mateusz-ramp/flutter_packages.git + ref: webview-target-blank + path: packages/webview_flutter/webview_flutter_android + webview_flutter_wkwebview: + git: + url: https://github.com/mateusz-ramp/flutter_packages.git + ref: webview-target-blank + path: packages/webview_flutter/webview_flutter_wkwebview + webview_flutter_platform_interface: + git: + url: https://github.com/mateusz-ramp/flutter_packages.git + ref: webview-target-blank + path: packages/webview_flutter/webview_flutter_platform_interface +``` + - Android document file upload from the WebView is not wired in this SDK yet. - Server-signed widget URLs are not supported in this release; use `Configuration` fields so the SDK can build the widget URL. diff --git a/example/pubspec.lock b/example/pubspec.lock index 50f0b1c..bba9db4 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -168,36 +168,40 @@ packages: source: hosted version: "1.1.1" webview_flutter: - dependency: transitive + dependency: "direct overridden" description: - name: webview_flutter - sha256: d53e1ccf5516f25017e3c9d44c39034db352d20fa34fe200674270242c2c5111 - url: "https://pub.dev" - source: hosted + path: "packages/webview_flutter/webview_flutter" + ref: webview-target-blank + resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c + url: "https://github.com/mateusz-ramp/flutter_packages.git" + source: git version: "4.14.1" webview_flutter_android: - dependency: transitive + dependency: "direct overridden" description: - name: webview_flutter_android - sha256: a97db7a44f8e71af2f3971c45550a08cce1fb60059c1b8e534251e6cfb753490 - url: "https://pub.dev" - source: hosted + path: "packages/webview_flutter/webview_flutter_android" + ref: webview-target-blank + resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c + url: "https://github.com/mateusz-ramp/flutter_packages.git" + source: git version: "4.13.0" webview_flutter_platform_interface: - dependency: transitive + dependency: "direct overridden" description: - name: webview_flutter_platform_interface - sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" - url: "https://pub.dev" - source: hosted + path: "packages/webview_flutter/webview_flutter_platform_interface" + ref: webview-target-blank + resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c + url: "https://github.com/mateusz-ramp/flutter_packages.git" + source: git version: "2.15.1" webview_flutter_wkwebview: - dependency: transitive + dependency: "direct overridden" description: - name: webview_flutter_wkwebview - sha256: c879dd64b87c452aa84381b244d5469da57ba7e8cca6884c7b1e0d406372c12d - url: "https://pub.dev" - source: hosted + path: "packages/webview_flutter/webview_flutter_wkwebview" + ref: webview-target-blank + resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c + url: "https://github.com/mateusz-ramp/flutter_packages.git" + source: git version: "3.26.0" sdks: dart: ">=3.12.0 <4.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 0af8c38..aa7591b 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -15,5 +15,29 @@ dependencies: dev_dependencies: flutter_lints: ^6.0.0 +# Overrides only apply in the *root* package. Required when running the example. +# Same fork as ramp_flutter — https://github.com/mateusz-ramp/flutter_packages/tree/webview-target-blank +dependency_overrides: + webview_flutter: + git: + url: https://github.com/mateusz-ramp/flutter_packages.git + ref: webview-target-blank + path: packages/webview_flutter/webview_flutter + webview_flutter_android: + git: + url: https://github.com/mateusz-ramp/flutter_packages.git + ref: webview-target-blank + path: packages/webview_flutter/webview_flutter_android + webview_flutter_wkwebview: + git: + url: https://github.com/mateusz-ramp/flutter_packages.git + ref: webview-target-blank + path: packages/webview_flutter/webview_flutter_wkwebview + webview_flutter_platform_interface: + git: + url: https://github.com/mateusz-ramp/flutter_packages.git + ref: webview-target-blank + path: packages/webview_flutter/webview_flutter_platform_interface + flutter: uses-material-design: true diff --git a/lib/internal/ramp_webview_controller.dart b/lib/internal/ramp_webview_controller.dart index 7a8f900..13d8972 100644 --- a/lib/internal/ramp_webview_controller.dart +++ b/lib/internal/ramp_webview_controller.dart @@ -50,7 +50,20 @@ class RampWebViewController { ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( - onNavigationRequest: (request) => _onNavigationRequest(request), + // In-WebView navigations stay in the WebView. + onNavigationRequest: (request) { + debugPrint('RampFlutter: allow navigation ${request.url}'); + return NavigationDecision.navigate; + }, + // New windows (`target=_blank` / `window.open`) open externally — + // same idea as native createWebView / onCreateWindow handlers. + onCreateWindow: (url) { + debugPrint('RampFlutter: open external (create window) $url'); + final uri = Uri.tryParse(url); + if (uri != null) { + _openExternal(uri); + } + }, onPageStarted: (url) => debugPrint('RampFlutter: page started $url'), onPageFinished: (url) => debugPrint('RampFlutter: page finished $url'), onWebResourceError: (error) { @@ -84,32 +97,6 @@ class RampWebViewController { return controller; } - Future _onNavigationRequest( - NavigationRequest request, - ) async { - final uri = Uri.tryParse(request.url); - if (uri == null) { - debugPrint('RampFlutter: blocking invalid navigation ${request.url}'); - return NavigationDecision.prevent; - } - - if (_isWidgetNavigation(uri)) { - debugPrint('RampFlutter: allow navigation $uri'); - return NavigationDecision.navigate; - } - - debugPrint('RampFlutter: open external $uri'); - await _openExternal(uri); - return NavigationDecision.prevent; - } - - bool _isWidgetNavigation(Uri uri) { - if (uri.scheme == 'about') return true; - if (uri.host.isEmpty) return true; - if (uri.host == _widgetUrl.host) return true; - return _trustedRampHost.hasMatch(uri.host); - } - Future _openExternal(Uri uri) async { try { if (uri.scheme == 'intent') { @@ -176,7 +163,3 @@ class RampWebViewController { onWidgetEvent?.call(Map.from(event)); } } - -final _trustedRampHost = RegExp( - r'^([a-z0-9-]+\.)*(ramp\.network|rampnetwork\.com|ramp-network\.org)$', -); diff --git a/pubspec.lock b/pubspec.lock index a1c6d43..4fb86c6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -288,34 +288,38 @@ packages: webview_flutter: dependency: "direct main" description: - name: webview_flutter - sha256: d53e1ccf5516f25017e3c9d44c39034db352d20fa34fe200674270242c2c5111 - url: "https://pub.dev" - source: hosted + path: "packages/webview_flutter/webview_flutter" + ref: webview-target-blank + resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c + url: "https://github.com/mateusz-ramp/flutter_packages.git" + source: git version: "4.14.1" webview_flutter_android: dependency: "direct main" description: - name: webview_flutter_android - sha256: a97db7a44f8e71af2f3971c45550a08cce1fb60059c1b8e534251e6cfb753490 - url: "https://pub.dev" - source: hosted + path: "packages/webview_flutter/webview_flutter_android" + ref: webview-target-blank + resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c + url: "https://github.com/mateusz-ramp/flutter_packages.git" + source: git version: "4.13.0" webview_flutter_platform_interface: - dependency: transitive + dependency: "direct overridden" description: - name: webview_flutter_platform_interface - sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" - url: "https://pub.dev" - source: hosted + path: "packages/webview_flutter/webview_flutter_platform_interface" + ref: webview-target-blank + resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c + url: "https://github.com/mateusz-ramp/flutter_packages.git" + source: git version: "2.15.1" webview_flutter_wkwebview: dependency: "direct main" description: - name: webview_flutter_wkwebview - sha256: c879dd64b87c452aa84381b244d5469da57ba7e8cca6884c7b1e0d406372c12d - url: "https://pub.dev" - source: hosted + path: "packages/webview_flutter/webview_flutter_wkwebview" + ref: webview-target-blank + resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c + url: "https://github.com/mateusz-ramp/flutter_packages.git" + source: git version: "3.26.0" sdks: dart: ">=3.12.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 77bf17c..3abbacc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,3 +19,29 @@ dev_dependencies: flutter_lints: ^6.0.0 flutter_test: sdk: flutter + +# Fork of flutter/packages with NavigationDelegate.onCreateWindow. +# Host apps must repeat these overrides (they only apply in the root package). +# Pin: https://github.com/mateusz-ramp/flutter_packages/tree/webview-target-blank +# (a37d157) +dependency_overrides: + webview_flutter: + git: + url: https://github.com/mateusz-ramp/flutter_packages.git + ref: webview-target-blank + path: packages/webview_flutter/webview_flutter + webview_flutter_android: + git: + url: https://github.com/mateusz-ramp/flutter_packages.git + ref: webview-target-blank + path: packages/webview_flutter/webview_flutter_android + webview_flutter_wkwebview: + git: + url: https://github.com/mateusz-ramp/flutter_packages.git + ref: webview-target-blank + path: packages/webview_flutter/webview_flutter_wkwebview + webview_flutter_platform_interface: + git: + url: https://github.com/mateusz-ramp/flutter_packages.git + ref: webview-target-blank + path: packages/webview_flutter/webview_flutter_platform_interface From a6907286bc5082a175386742fce16de73a015699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 15:10:49 +0200 Subject: [PATCH 13/56] recreate the project Co-authored-by: Cursor --- CHANGELOG.md | 59 +------ example/.gitignore | 5 +- example/.metadata | 33 ++++ example/README.md | 13 +- example/analysis_options.yaml | 27 ---- example/android/.gitignore | 3 +- example/android/app/build.gradle | 60 ------- example/android/app/build.gradle.kts | 45 ++++++ .../android/app/src/main/AndroidManifest.xml | 14 +- .../ramp/ramp_flutter_example/MainActivity.kt | 3 +- example/android/build.gradle | 30 ---- example/android/build.gradle.kts | 24 +++ example/android/gradle.properties | 7 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/android/settings.gradle | 29 ---- example/android/settings.gradle.kts | 26 +++ example/ios/Runner.xcodeproj/project.pbxproj | 148 +++++++++++++++++- .../xcshareddata/xcschemes/Runner.xcscheme | 13 +- example/ios/Runner/Info.plist | 11 +- example/ios/Runner/SceneDelegate.swift | 6 + example/ios/RunnerTests/RunnerTests.swift | 12 ++ example/lib/secrets.example.dart | 9 +- example/pubspec.lock | 127 ++++++++++++++- example/pubspec.yaml | 10 +- pubspec.lock | 2 +- pubspec.yaml | 11 +- ramp_flutter.iml | 3 +- 27 files changed, 481 insertions(+), 251 deletions(-) create mode 100644 example/.metadata delete mode 100644 example/android/app/build.gradle create mode 100644 example/android/app/build.gradle.kts delete mode 100644 example/android/build.gradle create mode 100644 example/android/build.gradle.kts delete mode 100644 example/android/settings.gradle create mode 100644 example/android/settings.gradle.kts create mode 100644 example/ios/Runner/SceneDelegate.swift create mode 100644 example/ios/RunnerTests/RunnerTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 09ea98d..e7595c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,57 +2,8 @@ ## 5.0.0 -* Rewrite the SDK to load the Ramp widget in a Flutter WebView instead of the - native iOS/Android Ramp SDKs. Published as a Dart Flutter package (no - Android/iOS plugin shells). -* Breaking: SDK no longer presents UI. Create `RampFlutter(configuration)`, - embed `ramp.view` in your own route/sheet, and call `dispose` when done. -* Add `Configuration.offrampAsset` and build the widget URL in Dart - (`Configuration.buildWidgetUrl`), including `sdkType` / `sdkVersion` / - `variant`. -* Default base URL is now `https://app.rampnetwork.com`. -* Raise minimum platforms to Android 7.0 (API 24) and iOS 13. -* Raise minimum Flutter to 3.44 / Dart 3.12; update `webview_flutter`, - `url_launcher`, and `flutter_lints`. -* Breaking: expose a single `onWidgetEvent` callback for all widget JS events; - remove specialized purchase/sale/send-crypto/close callbacks and related DTOs. -* Open `target=_blank` / `window.open` in the system browser via a forked - `webview_flutter` platform package (`flutter_packages` / - `webview-target-blank`). - -## 4.0.1 - -* Updated package documentation - -## 4.0.0 - -* Upgraded Android SDK dependency version to 4.0.+ - -## 3.0.0 - -* Regenerated plugin to fix platform specific dependencies - -## 2.0.2 - -* Update the default URL to the app - -## 2.0.1 - -* Fix missing `onPurchaseFailed` callback - -## 2.0.0 - -* Update native Ramp Network sdk to support off-ramp - -## 1.0.2 - -* Fixed Purchase object decoding for Android -* Updated URL in README - -## 1.0.1 - -* Fix `An operation is not implemented: Not yet implemented` exception - -## 1.0.0 - -* Initial release. +* Fresh package scaffolding via `flutter create --template=package` (Flutter + 3.44) with an `--empty` iOS/Android example app. +* Port of the Ramp Flutter WebView SDK: `Configuration`, `RampFlutter`, + widget events, `sendCrypto`, and forked `webview_flutter` `onCreateWindow` + handling for `target=_blank` / `window.open`. diff --git a/example/.gitignore b/example/.gitignore index f9d52b9..3820a95 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -27,14 +27,11 @@ migrate_working_dir/ **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ -.flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ - -# Local example secrets (copy from secrets.example.dart) -lib/secrets.dart +/coverage/ # Symbolication related app.*.symbols diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..3a28996 --- /dev/null +++ b/example/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "058e0af2c2b57e369d905a03ac9748b0ebf543c6" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + - platform: android + create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + - platform: ios + create_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + base_revision: 058e0af2c2b57e369d905a03ac9748b0ebf543c6 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/README.md b/example/README.md index cf12f1e..6acbd7d 100644 --- a/example/README.md +++ b/example/README.md @@ -1,14 +1,3 @@ # ramp_flutter_example -Demonstrates the Ramp Network Flutter WebView SDK (`ramp_flutter` 5.0.0). - -Use **Show Ramp** to present `ramp.view` in a modal bottom sheet (presentation -is owned by the example, not the SDK). - -## Local secrets - -Copy the template and add your host API key (file is gitignored): - -```bash -cp lib/secrets.example.dart lib/secrets.dart -``` +A new Flutter project. diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 0d29021..f9b3034 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1,28 +1 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/example/android/.gitignore b/example/android/.gitignore index 6f56801..be3943c 100644 --- a/example/android/.gitignore +++ b/example/android/.gitignore @@ -5,9 +5,10 @@ gradle-wrapper.jar /gradlew.bat /local.properties GeneratedPluginRegistrant.java +.cxx/ # Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +# See https://flutter.dev/to/reference-keystore key.properties **/*.keystore **/*.jks diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle deleted file mode 100644 index 14b6fdf..0000000 --- a/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -plugins { - id "com.android.application" - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" -} - -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -android { - namespace "network.ramp.ramp_flutter_example" - compileSdkVersion flutter.compileSdkVersion - ndkVersion flutter.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - applicationId "network.ramp.ramp_flutter_example" - minSdkVersion 24 - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} \ No newline at end of file diff --git a/example/android/app/build.gradle.kts b/example/android/app/build.gradle.kts new file mode 100644 index 0000000..2dd424c --- /dev/null +++ b/example/android/app/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "network.ramp.ramp_flutter_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "network.ramp.ramp_flutter_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = 24 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index a7d958a..5f46e37 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -2,13 +2,14 @@ + + + + + + + diff --git a/example/android/app/src/main/kotlin/network/ramp/ramp_flutter_example/MainActivity.kt b/example/android/app/src/main/kotlin/network/ramp/ramp_flutter_example/MainActivity.kt index 0d8ec47..618e9ee 100644 --- a/example/android/app/src/main/kotlin/network/ramp/ramp_flutter_example/MainActivity.kt +++ b/example/android/app/src/main/kotlin/network/ramp/ramp_flutter_example/MainActivity.kt @@ -2,5 +2,4 @@ package network.ramp.ramp_flutter_example import io.flutter.embedding.android.FlutterActivity -class MainActivity: FlutterActivity() { -} +class MainActivity : FlutterActivity() diff --git a/example/android/build.gradle b/example/android/build.gradle deleted file mode 100644 index e83fb5d..0000000 --- a/example/android/build.gradle +++ /dev/null @@ -1,30 +0,0 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} diff --git a/example/android/build.gradle.kts b/example/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 598d13f..e96108c 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,3 +1,6 @@ -org.gradle.jvmargs=-Xmx4G +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true -android.enableJetifier=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 3c472b9..2d428bf 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle deleted file mode 100644 index 7cd7128..0000000 --- a/example/android/settings.gradle +++ /dev/null @@ -1,29 +0,0 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - } - settings.ext.flutterSdkPath = flutterSdkPath() - - includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } - - plugins { - id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false - } -} - -plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false -} - -include ":app" diff --git a/example/android/settings.gradle.kts b/example/android/settings.gradle.kts new file mode 100644 index 0000000..c21f0c5 --- /dev/null +++ b/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false +} + +include(":app") diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 5e53361..75b34a0 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,14 +8,26 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -32,9 +44,12 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; @@ -58,6 +73,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -76,6 +99,7 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, ); sourceTree = ""; }; @@ -83,6 +107,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -97,6 +122,7 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; @@ -105,6 +131,23 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; @@ -138,6 +181,10 @@ LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; @@ -154,18 +201,26 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -214,17 +269,34 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -249,6 +321,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -268,7 +341,6 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -279,6 +351,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -303,7 +376,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 8MPBSFBFCZ; + DEVELOPMENT_TEAM = RC437W4FPJ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -318,10 +391,58 @@ }; name = Profile; }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = network.ramp.rampFlutterExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = network.ramp.rampFlutterExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = network.ramp.rampFlutterExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -341,7 +462,6 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -352,6 +472,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -378,6 +499,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -397,7 +519,6 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -408,6 +529,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -434,7 +556,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 8MPBSFBFCZ; + DEVELOPMENT_TEAM = RC437W4FPJ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -457,7 +579,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 8MPBSFBFCZ; + DEVELOPMENT_TEAM = RC437W4FPJ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -475,6 +597,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -498,7 +630,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 37e5248..c3fedb2 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -25,7 +25,7 @@ + + + + UISceneConfigurationName flutter UISceneDelegateClassName - FlutterSceneDelegate + $(PRODUCT_MODULE_NAME).SceneDelegate UISceneStoryboardFile Main @@ -75,6 +75,15 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight diff --git a/example/ios/Runner/SceneDelegate.swift b/example/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/example/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/example/lib/secrets.example.dart b/example/lib/secrets.example.dart index 76a9085..4730c95 100644 --- a/example/lib/secrets.example.dart +++ b/example/lib/secrets.example.dart @@ -1,8 +1,7 @@ -/// Template for local example secrets. +/// Local secrets for the example app. Do not commit this file. /// -/// Copy this file to `secrets.dart` and fill in real values: -/// `cp lib/secrets.example.dart lib/secrets.dart` +/// Copy [secrets.example.dart] to [secrets.dart] and fill in values. class ExampleSecrets { - /// iOS app `HOST_API_KEY_INTERNAL` — used with the dev widget host. - static const String hostApiKeyInternal = 'YOUR_DEV_HOST_API_KEY'; + /// Dev/internal widget host API key. + static const String hostApiKeyInternal = 'YOUR_HOST_API_KEY_INTERNAL'; } diff --git a/example/pubspec.lock b/example/pubspec.lock index bba9db4..0cbc714 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,6 +1,22 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" characters: dependency: transitive description: @@ -9,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" collection: dependency: transitive description: @@ -17,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -30,11 +62,40 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" flutter_web_plugins: dependency: transitive description: flutter source: sdk version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" lints: dependency: transitive description: @@ -43,6 +104,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" material_color_utilities: dependency: transitive description: @@ -87,6 +156,54 @@ packages: description: flutter source: sdk version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" + source: hosted + version: "0.7.11" url_launcher: dependency: transitive description: @@ -159,6 +276,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" web: dependency: transitive description: @@ -204,5 +329,5 @@ packages: source: git version: "3.26.0" sdks: - dart: ">=3.12.0 <4.0.0" + dart: ">=3.12.2 <4.0.0" flutter: ">=3.44.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index aa7591b..fb55d99 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,10 +1,10 @@ name: ramp_flutter_example description: "Demonstrates how to use the ramp_flutter package." -version: 1.0.0+1 -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: 'none' +version: 0.1.0+1 environment: - sdk: '>=3.12.0 <4.0.0' + sdk: ^3.12.2 dependencies: flutter: @@ -13,10 +13,12 @@ dependencies: path: ../ dev_dependencies: + flutter_test: + sdk: flutter flutter_lints: ^6.0.0 # Overrides only apply in the *root* package. Required when running the example. -# Same fork as ramp_flutter — https://github.com/mateusz-ramp/flutter_packages/tree/webview-target-blank +# https://github.com/mateusz-ramp/flutter_packages/tree/webview-target-blank dependency_overrides: webview_flutter: git: diff --git a/pubspec.lock b/pubspec.lock index 4fb86c6..9b7da79 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -322,5 +322,5 @@ packages: source: git version: "3.26.0" sdks: - dart: ">=3.12.0 <4.0.0" + dart: ">=3.12.2 <4.0.0" flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index 3abbacc..299c11f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: ramp_flutter -description: Ramp Network for Flutter loads the Ramp widget in a Flutter WebView with a unified Dart API for iOS and Android. +description: "Ramp Network for Flutter loads the Ramp widget in a Flutter WebView with a unified Dart API for iOS and Android." version: 5.0.0 homepage: https://ramp.network/ environment: - sdk: '>=3.12.0 <4.0.0' - flutter: '>=3.44.0' + sdk: ^3.12.2 + flutter: ">=3.44.0" dependencies: flutter: @@ -16,14 +16,13 @@ dependencies: webview_flutter_wkwebview: ^3.26.0 dev_dependencies: - flutter_lints: ^6.0.0 flutter_test: sdk: flutter + flutter_lints: ^6.0.0 # Fork of flutter/packages with NavigationDelegate.onCreateWindow. # Host apps must repeat these overrides (they only apply in the root package). -# Pin: https://github.com/mateusz-ramp/flutter_packages/tree/webview-target-blank -# (a37d157) +# https://github.com/mateusz-ramp/flutter_packages/tree/webview-target-blank dependency_overrides: webview_flutter: git: diff --git a/ramp_flutter.iml b/ramp_flutter.iml index 27686dd..118dd2c 100644 --- a/ramp_flutter.iml +++ b/ramp_flutter.iml @@ -4,11 +4,12 @@ + - + From bb48828b299d462039243b7cab35df5f51db7f4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 15:19:21 +0200 Subject: [PATCH 14/56] squash noop page --- lib/internal/ramp_webview_controller.dart | 165 --------------------- lib/internal/ramp_webview_page.dart | 16 --- lib/ramp_flutter.dart | 166 ++++++++++++++++++++-- test/ramp_webview_test.dart | 18 +-- 4 files changed, 165 insertions(+), 200 deletions(-) delete mode 100644 lib/internal/ramp_webview_controller.dart delete mode 100644 lib/internal/ramp_webview_page.dart diff --git a/lib/internal/ramp_webview_controller.dart b/lib/internal/ramp_webview_controller.dart deleted file mode 100644 index 13d8972..0000000 --- a/lib/internal/ramp_webview_controller.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:webview_flutter/webview_flutter.dart'; -import 'package:webview_flutter_android/webview_flutter_android.dart'; -import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; - -/// Owns the widget WebView and forwards Ramp Instant JS events to Dart. -class RampWebViewController { - RampWebViewController(this._widgetUrl); - - static const _channelName = 'RampInstantMobile'; - - final Uri _widgetUrl; - WebViewController? _webView; - - /// Called for every widget JS message. - Function(Map event)? onWidgetEvent; - - WebViewController get webViewController => - _webView ??= _createWebViewController(); - - WebViewController _createWebViewController() { - final PlatformWebViewControllerCreationParams params; - if (WebViewPlatform.instance is WebKitWebViewPlatform) { - params = WebKitWebViewControllerCreationParams( - allowsInlineMediaPlayback: true, - ); - } else { - params = const PlatformWebViewControllerCreationParams(); - } - - debugPrint('RampFlutter: loading $_widgetUrl'); - - final controller = WebViewController.fromPlatformCreationParams( - params, - onPermissionRequest: (request) { - final onlyCamera = request.types.every( - (type) => type == WebViewPermissionResourceType.camera, - ); - if (onlyCamera) { - request.grant(); - } else { - request.deny(); - } - }, - ) - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate( - NavigationDelegate( - // In-WebView navigations stay in the WebView. - onNavigationRequest: (request) { - debugPrint('RampFlutter: allow navigation ${request.url}'); - return NavigationDecision.navigate; - }, - // New windows (`target=_blank` / `window.open`) open externally — - // same idea as native createWebView / onCreateWindow handlers. - onCreateWindow: (url) { - debugPrint('RampFlutter: open external (create window) $url'); - final uri = Uri.tryParse(url); - if (uri != null) { - _openExternal(uri); - } - }, - onPageStarted: (url) => debugPrint('RampFlutter: page started $url'), - onPageFinished: (url) => debugPrint('RampFlutter: page finished $url'), - onWebResourceError: (error) { - debugPrint( - 'RampFlutter: resource error ' - 'code=${error.errorCode} type=${error.errorType} ' - 'desc=${error.description} url=${error.url}', - ); - }, - onHttpError: (error) { - debugPrint( - 'RampFlutter: HTTP error ' - 'status=${error.response?.statusCode} uri=${error.request?.uri}', - ); - }, - ), - ) - ..addJavaScriptChannel( - _channelName, - onMessageReceived: (message) => handleJavaScriptMessage(message.message), - ); - - final platform = controller.platform; - if (platform is AndroidWebViewController) { - platform.setMediaPlaybackRequiresUserGesture(false); - // Document upload on Android needs a host-provided file picker; not wired - // here. KYC camera is handled via onPermissionRequest. - } - - controller.loadRequest(_widgetUrl); - return controller; - } - - Future _openExternal(Uri uri) async { - try { - if (uri.scheme == 'intent') { - final fallback = _intentFallbackUrl(uri); - if (fallback != null) { - await launchUrl(fallback, mode: LaunchMode.externalApplication); - } - return; - } - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } - } catch (error, stackTrace) { - debugPrint('RampFlutter: failed to open $uri: $error\n$stackTrace'); - } - } - - static Uri? _intentFallbackUrl(Uri intentUri) { - final browserFallback = intentUri.queryParameters['browser_fallback_url']; - if (browserFallback != null && browserFallback.isNotEmpty) { - return Uri.tryParse(browserFallback); - } - return null; - } - - Future sendCrypto(String? transactionHash) { - final webView = _webView; - if (webView == null) { - return Future.value(); - } - final message = jsonEncode({ - 'type': 'SEND_CRYPTO_RESULT', - 'eventVersion': 1, - 'payload': {'txHash': transactionHash}, - }); - return webView.runJavaScript( - 'window.postMessage($message, "${_widgetUrl.origin}");', - ); - } - - /// Stops the page and releases capture. Call when the host dismisses the view. - void dispose() { - _webView - ?..removeJavaScriptChannel(_channelName) - ..loadRequest(Uri.parse('about:blank')); - _webView = null; - } - - @visibleForTesting - void handleJavaScriptMessage(String message) { - // The widget posts to both Android (`RampInstantMobile` + JSON.stringify) - // and iOS (`webkit.messageHandlers` + JS object). Flutter's channel - // receives both; the object path arrives as a non-JSON Map-style string. - // Prefer valid JSON event maps and ignore the rest. - final dynamic event; - try { - event = jsonDecode(message); - } on FormatException { - return; - } - if (event is! Map) { - return; - } - onWidgetEvent?.call(Map.from(event)); - } -} diff --git a/lib/internal/ramp_webview_page.dart b/lib/internal/ramp_webview_page.dart deleted file mode 100644 index 423e6c7..0000000 --- a/lib/internal/ramp_webview_page.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:webview_flutter/webview_flutter.dart'; - -import 'ramp_webview_controller.dart'; - -/// Fullscreen page that hosts the Ramp widget WebView. -class RampWebViewPage extends StatelessWidget { - const RampWebViewPage({super.key, required this.controller}); - - final RampWebViewController controller; - - @override - Widget build(BuildContext context) { - return WebViewWidget(controller: controller.webViewController); - } -} diff --git a/lib/ramp_flutter.dart b/lib/ramp_flutter.dart index 4745339..026204c 100644 --- a/lib/ramp_flutter.dart +++ b/lib/ramp_flutter.dart @@ -1,7 +1,11 @@ +import 'dart:convert'; + import 'package:flutter/widgets.dart'; import 'package:ramp_flutter/configuration.dart'; -import 'package:ramp_flutter/internal/ramp_webview_controller.dart'; -import 'package:ramp_flutter/internal/ramp_webview_page.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_android/webview_flutter_android.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; /// Flutter API for embedding the Ramp Network widget. /// @@ -9,26 +13,168 @@ import 'package:ramp_flutter/internal/ramp_webview_page.dart'; /// (route, bottom sheet, dialog, etc.) and must call [dispose] when done. class RampFlutter { RampFlutter(Configuration configuration) - : _controller = - RampWebViewController(configuration.buildWidgetUrl()) { - _controller.onWidgetEvent = (event) => onWidgetEvent?.call(event); - } + : this._(configuration.buildWidgetUrl()); + + /// Test-only: load a concrete widget URL without going through [Configuration]. + @visibleForTesting + RampFlutter.withWidgetUrl(Uri widgetUrl) : this._(widgetUrl); + + RampFlutter._(this._widgetUrl); + + static const _channelName = 'RampInstantMobile'; - final RampWebViewController _controller; + final Uri _widgetUrl; + WebViewController? _webView; /// Fires for every widget JS event. void Function(Map event)? onWidgetEvent; /// WebView that loads the Ramp widget. Embed this in your own UI. - Widget get view => RampWebViewPage(controller: _controller); + Widget get view => WebViewWidget(controller: _ensureWebView()); + + WebViewController _ensureWebView() => _webView ??= _createWebView(); + + WebViewController _createWebView() { + final PlatformWebViewControllerCreationParams params; + if (WebViewPlatform.instance is WebKitWebViewPlatform) { + params = WebKitWebViewControllerCreationParams( + allowsInlineMediaPlayback: true, + ); + } else { + params = const PlatformWebViewControllerCreationParams(); + } + + debugPrint('RampFlutter: loading $_widgetUrl'); + + final controller = WebViewController.fromPlatformCreationParams( + params, + onPermissionRequest: (request) { + final onlyCamera = request.types.every( + (type) => type == WebViewPermissionResourceType.camera, + ); + if (onlyCamera) { + request.grant(); + } else { + request.deny(); + } + }, + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + // In-WebView navigations stay in the WebView. + onNavigationRequest: (request) { + debugPrint('RampFlutter: allow navigation ${request.url}'); + return NavigationDecision.navigate; + }, + // New windows (`target=_blank` / `window.open`) open externally — + // same idea as native createWebView / onCreateWindow handlers. + onCreateWindow: (url) { + debugPrint('RampFlutter: open external (create window) $url'); + final uri = Uri.tryParse(url); + if (uri != null) { + _openExternal(uri); + } + }, + onPageStarted: (url) => debugPrint('RampFlutter: page started $url'), + onPageFinished: (url) => + debugPrint('RampFlutter: page finished $url'), + onWebResourceError: (error) { + debugPrint( + 'RampFlutter: resource error ' + 'code=${error.errorCode} type=${error.errorType} ' + 'desc=${error.description} url=${error.url}', + ); + }, + onHttpError: (error) { + debugPrint( + 'RampFlutter: HTTP error ' + 'status=${error.response?.statusCode} uri=${error.request?.uri}', + ); + }, + ), + ) + ..addJavaScriptChannel( + _channelName, + onMessageReceived: (message) => + handleJavaScriptMessage(message.message), + ); + + final platform = controller.platform; + if (platform is AndroidWebViewController) { + platform.setMediaPlaybackRequiresUserGesture(false); + // Document upload on Android needs a host-provided file picker; not wired + // here. KYC camera is handled via onPermissionRequest. + } + + controller.loadRequest(_widgetUrl); + return controller; + } + + Future _openExternal(Uri uri) async { + try { + if (uri.scheme == 'intent') { + final fallback = _intentFallbackUrl(uri); + if (fallback != null) { + await launchUrl(fallback, mode: LaunchMode.externalApplication); + } + return; + } + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } catch (error, stackTrace) { + debugPrint('RampFlutter: failed to open $uri: $error\n$stackTrace'); + } + } + + static Uri? _intentFallbackUrl(Uri intentUri) { + final browserFallback = intentUri.queryParameters['browser_fallback_url']; + if (browserFallback != null && browserFallback.isNotEmpty) { + return Uri.tryParse(browserFallback); + } + return null; + } /// Completes an off-ramp send-crypto request with an optional [transactionHash]. Future sendCrypto(String? transactionHash) { - return _controller.sendCrypto(transactionHash); + final webView = _webView; + if (webView == null) { + return Future.value(); + } + final message = jsonEncode({ + 'type': 'SEND_CRYPTO_RESULT', + 'eventVersion': 1, + 'payload': {'txHash': transactionHash}, + }); + return webView.runJavaScript( + 'window.postMessage($message, "${_widgetUrl.origin}");', + ); } /// Releases the WebView. Call when your presentation is dismissed. void dispose() { - _controller.dispose(); + _webView + ?..removeJavaScriptChannel(_channelName) + ..loadRequest(Uri.parse('about:blank')); + _webView = null; + } + + @visibleForTesting + void handleJavaScriptMessage(String message) { + // The widget posts to both Android (`RampInstantMobile` + JSON.stringify) + // and iOS (`webkit.messageHandlers` + JS object). Flutter's channel + // receives both; the object path arrives as a non-JSON Map-style string. + // Prefer valid JSON event maps and ignore the rest. + final dynamic event; + try { + event = jsonDecode(message); + } on FormatException { + return; + } + if (event is! Map) { + return; + } + onWidgetEvent?.call(Map.from(event)); } } diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index 62a9e98..eaf7f15 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:ramp_flutter/configuration.dart'; -import 'package:ramp_flutter/internal/ramp_webview_controller.dart'; +import 'package:ramp_flutter/ramp_flutter.dart'; void main() { group('Configuration.buildWidgetUrl', () { @@ -55,29 +55,29 @@ void main() { }); }); - group('RampWebViewController events', () { + group('RampFlutter events', () { final widgetUrl = Uri.parse('https://app.rampnetwork.com/'); test('forwards JSON event maps and ignores non-JSON twins', () { final events = >[]; - final controller = RampWebViewController(widgetUrl) + final ramp = RampFlutter.withWidgetUrl(widgetUrl) ..onWidgetEvent = events.add; - controller.handleJavaScriptMessage('not json'); - controller.handleJavaScriptMessage('42'); - controller.handleJavaScriptMessage( + ramp.handleJavaScriptMessage('not json'); + ramp.handleJavaScriptMessage('42'); + ramp.handleJavaScriptMessage( '{widgetInstanceId: abc, type: WIDGET_CONFIG_DONE, payload: null}', ); - controller.handleJavaScriptMessage(jsonEncode({ + ramp.handleJavaScriptMessage(jsonEncode({ 'type': 'WIDGET_CONFIG_DONE', 'payload': null, 'widgetInstanceId': 'abc', })); - controller.handleJavaScriptMessage(jsonEncode({ + ramp.handleJavaScriptMessage(jsonEncode({ 'type': 'PURCHASE_CREATED', 'payload': {'purchase': {'id': 'purchase-id'}}, })); - controller.handleJavaScriptMessage(jsonEncode({'type': 'CLOSE'})); + ramp.handleJavaScriptMessage(jsonEncode({'type': 'CLOSE'})); expect(events, [ { From a8ab661999a2729552e4dd0cac1bdd896454cad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 15:41:17 +0200 Subject: [PATCH 15/56] extract events --- CHANGELOG.md | 35 +++++- README.md | 15 ++- analysis_options.yaml | 4 +- example/analysis_options.yaml | 3 + example/lib/main.dart | 172 ++++++++------------------ example/lib/secrets.example.dart | 5 +- lib/configuration.dart | 16 +-- lib/events/json_map.dart | 9 ++ lib/events/offramp_sale_created.dart | 5 + lib/events/purchase_created.dart | 5 + lib/events/ramp_closed.dart | 5 + lib/events/send_crypto_requested.dart | 48 +++++++ lib/ramp_flutter.dart | 141 +++++++++------------ lib/widget_event.dart | 47 +++++++ test/ramp_webview_test.dart | 130 +++++++++++-------- 15 files changed, 360 insertions(+), 280 deletions(-) create mode 100644 lib/events/json_map.dart create mode 100644 lib/events/offramp_sale_created.dart create mode 100644 lib/events/purchase_created.dart create mode 100644 lib/events/ramp_closed.dart create mode 100644 lib/events/send_crypto_requested.dart create mode 100644 lib/widget_event.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index e7595c4..f1b2b0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,33 @@ ## 5.0.0 -* Fresh package scaffolding via `flutter create --template=package` (Flutter - 3.44) with an `--empty` iOS/Android example app. -* Port of the Ramp Flutter WebView SDK: `Configuration`, `RampFlutter`, - widget events, `sendCrypto`, and forked `webview_flutter` `onCreateWindow` - handling for `target=_blank` / `window.open`. +* Rewrite the SDK to load the Ramp widget in a Flutter WebView instead of the + native iOS/Android Ramp SDKs. Published as a Dart Flutter package (no + Android/iOS plugin shells). +* Breaking: SDK no longer presents UI. Create `RampFlutter(configuration)`, + embed `ramp.view` in your own route/sheet, and call `dispose` when done. +* Widget JS events match the original Flutter SDK surface: `PurchaseCreated`, + `OfframpSaleCreated`, `SendCryptoRequested`, and `RampClosed` + (`WIDGET_CLOSE` / `CLOSE`). Purchase and sale are signals only. +* Add `Configuration.offrampAsset` and build the widget URL in Dart + (`Configuration.buildWidgetUrl`), including `sdkType` / `sdkVersion` / + `variant`. +* Default base URL is now `https://app.rampnetwork.com`. +* Raise minimum platforms to Android 7.0 (API 24) and iOS 13. +* Raise minimum Flutter to 3.44 / Dart 3.12; update `webview_flutter`, + `url_launcher`, and `flutter_lints`. +* Open `target=_blank` / `window.open` in the system browser via a forked + `webview_flutter` platform package (`flutter_packages` / + `webview-target-blank`). + +## 4.0.1 + +* Updated package documentation + +## 4.0.0 + +* Upgraded Android SDK dependency version to 4.0.+ + +## 3.0.0 + +* Regenerated plugin to fix platform specific dependencies diff --git a/README.md b/README.md index 4ec0582..e3d7338 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,15 @@ sheet, dialog, etc.) and must call `dispose` when it is dismissed. ```dart final ramp = RampFlutter(configuration) ..onWidgetEvent = (event) { - // Every widget JS event, e.g. PURCHASE_CREATED, SEND_CRYPTO, CLOSE, ... - if (event['type'] == 'SEND_CRYPTO') { - ramp.sendCrypto(txHash); - } - if (event['type'] == 'CLOSE' || event['type'] == 'WIDGET_CLOSE') { - Navigator.of(context).pop(); + switch (event) { + case PurchaseCreated(): + break; + case OfframpSaleCreated(): + break; + case SendCryptoRequested(:final payload): + ramp.sendCrypto(txHash); + case RampClosed(): + Navigator.of(context).pop(); } }; diff --git a/analysis_options.yaml b/analysis_options.yaml index a5744c1..76940e6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,4 @@ include: package:flutter_lints/flutter.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +formatter: + page_width: 120 diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index f9b3034..76940e6 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1 +1,4 @@ include: package:flutter_lints/flutter.yaml + +formatter: + page_width: 120 diff --git a/example/lib/main.dart b/example/lib/main.dart index 64b4d7c..2a6b092 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:ramp_flutter/configuration.dart'; import 'package:ramp_flutter/ramp_flutter.dart'; +import 'package:ramp_flutter/widget_event.dart'; import 'secrets.dart'; @@ -21,8 +22,7 @@ class RampFlutterApp extends StatefulWidget { class _RampFlutterAppState extends State { final Configuration _configuration = Configuration(); - final ValueNotifier> _debugEvents = - ValueNotifier>(const []); + final ValueNotifier> _debugEvents = ValueNotifier>(const []); var _nextDebugEventId = 0; final List _predefinedEnvironments = [ @@ -36,8 +36,7 @@ class _RampFlutterAppState extends State { @override void initState() { _configuration.hostAppName = "Ramp Network Flutter"; - _configuration.hostLogoUrl = - "https://assets.rampnetwork.com/misc/ramp-network-logo.svg"; + _configuration.hostLogoUrl = "https://assets.rampnetwork.com/misc/ramp-network-logo.svg"; _configuration.defaultFlow = "ONRAMP"; _configuration.enabledFlows = ["ONRAMP", "OFFRAMP", "SWAP"]; _configuration.defaultAsset = "BTC_BTC"; @@ -62,37 +61,38 @@ class _RampFlutterAppState extends State { void _applyEnvironment(int id) { _selectedEnvironment = id; _configuration.url = _predefinedEnvironments[id]; - // Dev/internal key only; demo/prod need their own keys. - _configuration.hostApiKey = - id == 0 ? ExampleSecrets.hostApiKeyInternal : null; + _configuration.hostApiKey = id == 0 ? ExampleSecrets.hostApiKeyInternal : null; } - void _addDebugEvent(Map event) { - final encoded = const JsonEncoder.withIndent(' ').convert(event); + void _addDebugEvent(String label, [Map data = const {}]) { + final encoded = const JsonEncoder.withIndent(' ').convert({'event': label, ...data}); debugPrint('Ramp example event:\n$encoded'); - _debugEvents.value = [ - ..._debugEvents.value, - _DebugEvent(_nextDebugEventId++, encoded), - ]; + _debugEvents.value = [..._debugEvents.value, _DebugEvent(_nextDebugEventId++, encoded)]; } void _removeDebugEvent(int id) { - _debugEvents.value = - _debugEvents.value.where((e) => e.id != id).toList(growable: false); + _debugEvents.value = _debugEvents.value.where((e) => e.id != id).toList(growable: false); } Future _showRamp(BuildContext context) async { final ramp = RampFlutter(_configuration); ramp.onWidgetEvent = (event) { - _addDebugEvent(event); - - final type = event['type']; - if (type == 'SEND_CRYPTO') { - ramp.sendCrypto('123'); + switch (event) { + case PurchaseCreated(): + _addDebugEvent('PURCHASE_CREATED'); + case OfframpSaleCreated(): + _addDebugEvent('OFFRAMP_SALE_CREATED'); + case SendCryptoRequested(:final payload): + _addDebugEvent('SEND_CRYPTO', { + 'address': payload.address, + 'amount': payload.amount, + 'asset': payload.assetInfo?.symbol, + }); + ramp.sendCrypto('123'); + case RampClosed(): + _addDebugEvent('CLOSE'); } - // Do not auto-pop on CLOSE/WIDGET_CLOSE — keep banners visible; - // dismiss the sheet manually. }; await showModalBottomSheet( @@ -151,16 +151,12 @@ class _RampFlutterAppState extends State { return MaterialApp( home: Builder( builder: (context) => Scaffold( - appBar: AppBar( - title: const Text('Ramp Network Flutter'), - ), + appBar: AppBar(title: const Text('Ramp Network Flutter')), body: Stack( children: [ Padding( padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), - child: ListView( - children: _formFields(context), - ), + child: ListView(children: _formFields(context)), ), Positioned( left: 8, @@ -189,69 +185,31 @@ class _RampFlutterAppState extends State { List _configurationForm() { return [ - _segmentedControl( - "Env:", - ["dev", "demo", "prod"], - _selectEnvironment, - ), + _segmentedControl("Env:", ["dev", "demo", "prod"], _selectEnvironment), Text( _predefinedEnvironments[_selectedEnvironment], - style: const TextStyle( - color: Color.fromRGBO(46, 190, 117, 1), - ), + style: const TextStyle(color: Color.fromRGBO(46, 190, 117, 1)), ), _textField( "User email address", (text) => _configuration.userEmailAddress = text, _configuration.userEmailAddress, ), - _textField( - "Fiat value", - (text) => _configuration.fiatValue = text, - _configuration.fiatValue, - ), - _textField( - "Fiat currency", - (text) => _configuration.fiatCurrency = text, - _configuration.fiatCurrency, - ), - _textField( - "Default asset", - (text) => _configuration.defaultAsset = text, - _configuration.defaultAsset, - ), - _textField( - "Offramp asset", - (text) => _configuration.offrampAsset = text, - _configuration.offrampAsset, - ), - _textField( - "User address", - (text) => _configuration.userAddress = text, - _configuration.userAddress, - ), - _textField( - "Host app name", - (text) => _configuration.hostAppName = text, - _configuration.hostAppName, - ), - _textField( - "Host API key", - (text) => _configuration.hostApiKey = text, - _configuration.hostApiKey, - ), - _segmentedControl( - "Default flow:", - ["ONRAMP", "OFFRAMP"], - (index) { - if (index == 0) { - _configuration.defaultFlow = "ONRAMP"; - } - if (index == 1) { - _configuration.defaultFlow = "OFFRAMP"; - } - }, - ), + _textField("Fiat value", (text) => _configuration.fiatValue = text, _configuration.fiatValue), + _textField("Fiat currency", (text) => _configuration.fiatCurrency = text, _configuration.fiatCurrency), + _textField("Default asset", (text) => _configuration.defaultAsset = text, _configuration.defaultAsset), + _textField("Offramp asset", (text) => _configuration.offrampAsset = text, _configuration.offrampAsset), + _textField("User address", (text) => _configuration.userAddress = text, _configuration.userAddress), + _textField("Host app name", (text) => _configuration.hostAppName = text, _configuration.hostAppName), + _textField("Host API key", (text) => _configuration.hostApiKey = text, _configuration.hostApiKey), + _segmentedControl("Default flow:", ["ONRAMP", "OFFRAMP"], (index) { + if (index == 0) { + _configuration.defaultFlow = "ONRAMP"; + } + if (index == 1) { + _configuration.defaultFlow = "OFFRAMP"; + } + }), _enabledFlows(), ]; } @@ -284,43 +242,23 @@ class _RampFlutterAppState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text("Enabled flows:"), - Row( - children: [ - flowSwitch("ONRAMP"), - flowSwitch("OFFRAMP"), - flowSwitch("SWAP"), - ], - ), + Row(children: [flowSwitch("ONRAMP"), flowSwitch("OFFRAMP"), flowSwitch("SWAP")]), ], ); } Widget _showRampButton(BuildContext context) { - return TextButton( - onPressed: () => _showRamp(context), - child: const Text("Show Ramp"), - ); + return TextButton(onPressed: () => _showRamp(context), child: const Text("Show Ramp")); } - Row _segmentedControl( - String title, - List options, - void Function(int) itemSelected, - ) { + Row _segmentedControl(String title, List options, void Function(int) itemSelected) { final segments = options.asMap().entries.map((entry) { - return TextButton( - onPressed: () => itemSelected(entry.key), - child: Text(entry.value), - ); + return TextButton(onPressed: () => itemSelected(entry.key), child: Text(entry.value)); }).toList(); return Row(children: [Text(title), ...segments]); } - TextField _textField( - String placeholder, - void Function(String) onChanged, - String? defaultValue, - ) { + TextField _textField(String placeholder, void Function(String) onChanged, String? defaultValue) { return TextField( decoration: InputDecoration(hintText: placeholder), onChanged: onChanged, @@ -337,11 +275,7 @@ class _DebugEvent { } class _DebugEventList extends StatelessWidget { - const _DebugEventList({ - required this.eventsListenable, - required this.maxHeight, - required this.onDismiss, - }); + const _DebugEventList({required this.eventsListenable, required this.maxHeight, required this.onDismiss}); final ValueNotifier> eventsListenable; final double maxHeight; @@ -375,20 +309,12 @@ class _DebugEventList extends StatelessWidget { Expanded( child: Text( event.body, - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontFamily: 'Courier', - ), + style: const TextStyle(color: Colors.white, fontSize: 11, fontFamily: 'Courier'), ), ), IconButton( visualDensity: VisualDensity.compact, - icon: const Icon( - Icons.close, - color: Colors.white70, - size: 18, - ), + icon: const Icon(Icons.close, color: Colors.white70, size: 18), onPressed: () => onDismiss(event.id), ), ], diff --git a/example/lib/secrets.example.dart b/example/lib/secrets.example.dart index 4730c95..92e8c6d 100644 --- a/example/lib/secrets.example.dart +++ b/example/lib/secrets.example.dart @@ -1,7 +1,4 @@ -/// Local secrets for the example app. Do not commit this file. -/// -/// Copy [secrets.example.dart] to [secrets.dart] and fill in values. +/// Copy to [secrets.dart] and fill in values. Do not commit secrets.dart. class ExampleSecrets { - /// Dev/internal widget host API key. static const String hostApiKeyInternal = 'YOUR_HOST_API_KEY_INTERNAL'; } diff --git a/lib/configuration.dart b/lib/configuration.dart index ee22797..0685fe8 100644 --- a/lib/configuration.dart +++ b/lib/configuration.dart @@ -4,7 +4,6 @@ class Configuration { static const String sdkVersion = '5.0.0'; static const String mobileSdkVariant = 'sdk-mobile'; - /// Base widget URL (scheme + host + optional path). Query is built from fields below. String? url; String? containerNode; @@ -26,15 +25,13 @@ class Configuration { String? userAddress; String? userEmailAddress; bool? useSendCryptoCallback; + /// Ignored when building the URL; the SDK always sends [mobileSdkVariant]. String? variant; String? webhookStatusUrl; - /// Builds the widget URL from [url] and configuration query parameters. Uri buildWidgetUrl() { - final base = Uri.parse( - (url != null && url!.trim().isNotEmpty) ? url!.trim() : defaultUrl, - ); + final base = Uri.parse((url != null && url!.trim().isNotEmpty) ? url!.trim() : defaultUrl); final queryParameters = { ...base.queryParameters, @@ -42,8 +39,7 @@ class Configuration { if (_nonEmpty(deepLinkScheme)) 'deepLinkScheme': deepLinkScheme!, if (_nonEmpty(defaultAsset)) 'defaultAsset': defaultAsset!, if (_nonEmpty(defaultFlow)) 'defaultFlow': defaultFlow!, - if (enabledFlows != null && enabledFlows!.isNotEmpty) - 'enabledFlows': enabledFlows!.join(','), + if (enabledFlows != null && enabledFlows!.isNotEmpty) 'enabledFlows': enabledFlows!.join(','), if (_nonEmpty(fiatCurrency)) 'fiatCurrency': fiatCurrency!, if (_nonEmpty(fiatValue)) 'fiatValue': fiatValue!, if (_nonEmpty(finalUrl)) 'finalUrl': finalUrl!, @@ -51,10 +47,8 @@ class Configuration { if (_nonEmpty(hostAppName)) 'hostAppName': hostAppName!, if (_nonEmpty(hostLogoUrl)) 'hostLogoUrl': hostLogoUrl!, if (_nonEmpty(offrampAsset)) 'offrampAsset': offrampAsset!, - if (_nonEmpty(offrampWebhookV3Url)) - 'offrampWebhookV3Url': offrampWebhookV3Url!, - if (_nonEmpty(selectedCountryCode)) - 'selectedCountryCode': selectedCountryCode!, + if (_nonEmpty(offrampWebhookV3Url)) 'offrampWebhookV3Url': offrampWebhookV3Url!, + if (_nonEmpty(selectedCountryCode)) 'selectedCountryCode': selectedCountryCode!, if (_nonEmpty(swapAmount)) 'swapAmount': swapAmount!, if (_nonEmpty(swapAsset)) 'swapAsset': swapAsset!, if (_nonEmpty(userAddress)) 'userAddress': userAddress!, diff --git a/lib/events/json_map.dart b/lib/events/json_map.dart new file mode 100644 index 0000000..f04628d --- /dev/null +++ b/lib/events/json_map.dart @@ -0,0 +1,9 @@ +Map? asStringKeyedMap(dynamic value) { + if (value is Map) { + return value; + } + if (value is Map) { + return Map.from(value); + } + return null; +} diff --git a/lib/events/offramp_sale_created.dart b/lib/events/offramp_sale_created.dart new file mode 100644 index 0000000..8139af2 --- /dev/null +++ b/lib/events/offramp_sale_created.dart @@ -0,0 +1,5 @@ +part of '../widget_event.dart'; + +final class OfframpSaleCreated extends WidgetEvent { + const OfframpSaleCreated(); +} diff --git a/lib/events/purchase_created.dart b/lib/events/purchase_created.dart new file mode 100644 index 0000000..3b1b365 --- /dev/null +++ b/lib/events/purchase_created.dart @@ -0,0 +1,5 @@ +part of '../widget_event.dart'; + +final class PurchaseCreated extends WidgetEvent { + const PurchaseCreated(); +} diff --git a/lib/events/ramp_closed.dart b/lib/events/ramp_closed.dart new file mode 100644 index 0000000..c33ae60 --- /dev/null +++ b/lib/events/ramp_closed.dart @@ -0,0 +1,5 @@ +part of '../widget_event.dart'; + +final class RampClosed extends WidgetEvent { + const RampClosed(); +} diff --git a/lib/events/send_crypto_requested.dart b/lib/events/send_crypto_requested.dart new file mode 100644 index 0000000..6987e72 --- /dev/null +++ b/lib/events/send_crypto_requested.dart @@ -0,0 +1,48 @@ +part of '../widget_event.dart'; + +final class SendCryptoRequested extends WidgetEvent { + const SendCryptoRequested(this.payload); + final SendCryptoPayload payload; +} + +class SendCryptoPayload { + const SendCryptoPayload({this.address, this.amount, this.assetInfo}); + + final String? address; + final String? amount; + final SendCryptoAssetInfo? assetInfo; + + factory SendCryptoPayload.fromJson(Map? json) { + if (json == null) { + return const SendCryptoPayload(); + } + return SendCryptoPayload( + address: json['address'] as String?, + amount: json['amount'] as String?, + assetInfo: SendCryptoAssetInfo.fromJson(asStringKeyedMap(json['assetInfo'])), + ); + } +} + +class SendCryptoAssetInfo { + const SendCryptoAssetInfo({this.chain, this.decimals, this.name, this.symbol, this.type}); + + final String? chain; + final int? decimals; + final String? name; + final String? symbol; + final String? type; + + factory SendCryptoAssetInfo.fromJson(Map? json) { + if (json == null) { + return const SendCryptoAssetInfo(); + } + return SendCryptoAssetInfo( + chain: json['chain'] as String?, + decimals: (json['decimals'] as num?)?.toInt(), + name: json['name'] as String?, + symbol: json['symbol'] as String?, + type: json['type'] as String?, + ); + } +} diff --git a/lib/ramp_flutter.dart b/lib/ramp_flutter.dart index 026204c..c94d1fd 100644 --- a/lib/ramp_flutter.dart +++ b/lib/ramp_flutter.dart @@ -2,18 +2,14 @@ import 'dart:convert'; import 'package:flutter/widgets.dart'; import 'package:ramp_flutter/configuration.dart'; +import 'package:ramp_flutter/widget_event.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter_android/webview_flutter_android.dart'; import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; -/// Flutter API for embedding the Ramp Network widget. -/// -/// The SDK provides the widget [view]; the host app owns presentation -/// (route, bottom sheet, dialog, etc.) and must call [dispose] when done. class RampFlutter { - RampFlutter(Configuration configuration) - : this._(configuration.buildWidgetUrl()); + RampFlutter(Configuration configuration) : this._(configuration.buildWidgetUrl()); /// Test-only: load a concrete widget URL without going through [Configuration]. @visibleForTesting @@ -26,10 +22,8 @@ class RampFlutter { final Uri _widgetUrl; WebViewController? _webView; - /// Fires for every widget JS event. - void Function(Map event)? onWidgetEvent; + void Function(WidgetEvent event)? onWidgetEvent; - /// WebView that loads the Ramp widget. Embed this in your own UI. Widget get view => WebViewWidget(controller: _ensureWebView()); WebViewController _ensureWebView() => _webView ??= _createWebView(); @@ -37,74 +31,64 @@ class RampFlutter { WebViewController _createWebView() { final PlatformWebViewControllerCreationParams params; if (WebViewPlatform.instance is WebKitWebViewPlatform) { - params = WebKitWebViewControllerCreationParams( - allowsInlineMediaPlayback: true, - ); + params = WebKitWebViewControllerCreationParams(allowsInlineMediaPlayback: true); } else { params = const PlatformWebViewControllerCreationParams(); } debugPrint('RampFlutter: loading $_widgetUrl'); - final controller = WebViewController.fromPlatformCreationParams( - params, - onPermissionRequest: (request) { - final onlyCamera = request.types.every( - (type) => type == WebViewPermissionResourceType.camera, - ); - if (onlyCamera) { - request.grant(); - } else { - request.deny(); - } - }, - ) - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate( - NavigationDelegate( - // In-WebView navigations stay in the WebView. - onNavigationRequest: (request) { - debugPrint('RampFlutter: allow navigation ${request.url}'); - return NavigationDecision.navigate; - }, - // New windows (`target=_blank` / `window.open`) open externally — - // same idea as native createWebView / onCreateWindow handlers. - onCreateWindow: (url) { - debugPrint('RampFlutter: open external (create window) $url'); - final uri = Uri.tryParse(url); - if (uri != null) { - _openExternal(uri); - } - }, - onPageStarted: (url) => debugPrint('RampFlutter: page started $url'), - onPageFinished: (url) => - debugPrint('RampFlutter: page finished $url'), - onWebResourceError: (error) { - debugPrint( - 'RampFlutter: resource error ' - 'code=${error.errorCode} type=${error.errorType} ' - 'desc=${error.description} url=${error.url}', - ); - }, - onHttpError: (error) { - debugPrint( - 'RampFlutter: HTTP error ' - 'status=${error.response?.statusCode} uri=${error.request?.uri}', - ); - }, - ), - ) - ..addJavaScriptChannel( - _channelName, - onMessageReceived: (message) => - handleJavaScriptMessage(message.message), - ); + final controller = + WebViewController.fromPlatformCreationParams( + params, + onPermissionRequest: (request) { + final onlyCamera = request.types.every((type) => type == WebViewPermissionResourceType.camera); + if (onlyCamera) { + request.grant(); + } else { + request.deny(); + } + }, + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onNavigationRequest: (request) { + debugPrint('RampFlutter: allow navigation ${request.url}'); + return NavigationDecision.navigate; + }, + onCreateWindow: (url) { + debugPrint('RampFlutter: open external (create window) $url'); + final uri = Uri.tryParse(url); + if (uri != null) { + _openExternal(uri); + } + }, + onPageStarted: (url) => debugPrint('RampFlutter: page started $url'), + onPageFinished: (url) => debugPrint('RampFlutter: page finished $url'), + onWebResourceError: (error) { + debugPrint( + 'RampFlutter: resource error ' + 'code=${error.errorCode} type=${error.errorType} ' + 'desc=${error.description} url=${error.url}', + ); + }, + onHttpError: (error) { + debugPrint( + 'RampFlutter: HTTP error ' + 'status=${error.response?.statusCode} uri=${error.request?.uri}', + ); + }, + ), + ) + ..addJavaScriptChannel( + _channelName, + onMessageReceived: (message) => handleJavaScriptMessage(message.message), + ); final platform = controller.platform; if (platform is AndroidWebViewController) { platform.setMediaPlaybackRequiresUserGesture(false); - // Document upload on Android needs a host-provided file picker; not wired - // here. KYC camera is handled via onPermissionRequest. } controller.loadRequest(_widgetUrl); @@ -136,7 +120,6 @@ class RampFlutter { return null; } - /// Completes an off-ramp send-crypto request with an optional [transactionHash]. Future sendCrypto(String? transactionHash) { final webView = _webView; if (webView == null) { @@ -147,12 +130,9 @@ class RampFlutter { 'eventVersion': 1, 'payload': {'txHash': transactionHash}, }); - return webView.runJavaScript( - 'window.postMessage($message, "${_widgetUrl.origin}");', - ); + return webView.runJavaScript('window.postMessage($message, "${_widgetUrl.origin}");'); } - /// Releases the WebView. Call when your presentation is dismissed. void dispose() { _webView ?..removeJavaScriptChannel(_channelName) @@ -162,19 +142,20 @@ class RampFlutter { @visibleForTesting void handleJavaScriptMessage(String message) { - // The widget posts to both Android (`RampInstantMobile` + JSON.stringify) - // and iOS (`webkit.messageHandlers` + JS object). Flutter's channel - // receives both; the object path arrives as a non-JSON Map-style string. - // Prefer valid JSON event maps and ignore the rest. - final dynamic event; + // Non-JSON twins from iOS object posts are ignored; keep JSON event maps. + final dynamic decoded; try { - event = jsonDecode(message); + decoded = jsonDecode(message); } on FormatException { return; } - if (event is! Map) { + if (decoded is! Map) { + return; + } + final event = WidgetEvent.tryParse(Map.from(decoded)); + if (event == null) { return; } - onWidgetEvent?.call(Map.from(event)); + onWidgetEvent?.call(event); } } diff --git a/lib/widget_event.dart b/lib/widget_event.dart new file mode 100644 index 0000000..f55d012 --- /dev/null +++ b/lib/widget_event.dart @@ -0,0 +1,47 @@ +import 'package:ramp_flutter/events/json_map.dart'; + +part 'events/offramp_sale_created.dart'; +part 'events/purchase_created.dart'; +part 'events/ramp_closed.dart'; +part 'events/send_crypto_requested.dart'; + +/// Incoming widget events from the original Flutter SDK surface. +/// +/// `PURCHASE_CREATED` / `OFFRAMP_SALE_CREATED` / `WIDGET_CLOSE` are signals +/// only (no payload decoding). +sealed class WidgetEvent { + const WidgetEvent(); + + static WidgetEvent? tryParse(Map json) { + final type = json['type']; + if (type is! String) { + return null; + } + final payload = asStringKeyedMap(json['payload']); + + try { + switch (type) { + case 'OFFRAMP_SALE_CREATED': + return const OfframpSaleCreated(); + case 'PURCHASE_CREATED': + return const PurchaseCreated(); + case 'SEND_CRYPTO': + final version = json['eventVersion']; + if (version != null && version != 1) { + return null; + } + if (payload == null) { + return null; + } + return SendCryptoRequested(SendCryptoPayload.fromJson(payload)); + case 'WIDGET_CLOSE': + case 'CLOSE': + return const RampClosed(); + default: + return null; + } + } catch (_) { + return null; + } + } +} diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index eaf7f15..e206a53 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:ramp_flutter/configuration.dart'; import 'package:ramp_flutter/ramp_flutter.dart'; +import 'package:ramp_flutter/widget_event.dart'; void main() { group('Configuration.buildWidgetUrl', () { @@ -17,82 +18,113 @@ void main() { }); test('merges configuration fields and joins enabled flows', () { - final url = (Configuration() - ..url = 'https://app.dev.ramp-network.org/custom' - ..hostApiKey = 'key' - ..hostAppName = 'App' - ..offrampAsset = 'ETH' - ..offrampWebhookV3Url = 'https://example.com/hook' - ..enabledFlows = ['ONRAMP', 'OFFRAMP'] - ..defaultFlow = 'OFFRAMP' - ..useSendCryptoCallback = true - ..variant = 'ignored') - .buildWidgetUrl(); + final url = + (Configuration() + ..url = 'https://app.dev.ramp-network.org/custom' + ..hostApiKey = 'key' + ..hostAppName = 'App' + ..offrampAsset = 'ETH' + ..offrampWebhookV3Url = 'https://example.com/hook' + ..enabledFlows = ['ONRAMP', 'OFFRAMP'] + ..defaultFlow = 'OFFRAMP' + ..useSendCryptoCallback = true + ..variant = 'ignored') + .buildWidgetUrl(); expect(url.host, 'app.dev.ramp-network.org'); expect(url.path, '/custom'); expect(url.queryParameters['hostApiKey'], 'key'); - expect(url.queryParameters['hostAppName'], 'App'); - expect(url.queryParameters['offrampAsset'], 'ETH'); - expect(url.queryParameters['offrampWebhookV3Url'], - 'https://example.com/hook'); expect(url.queryParameters['enabledFlows'], 'ONRAMP,OFFRAMP'); - expect(url.queryParameters['defaultFlow'], 'OFFRAMP'); expect(url.queryParameters['useSendCryptoCallbackVersion'], '1'); expect(url.queryParameters['variant'], 'sdk-mobile'); }); test('omits null and empty optional fields', () { - final url = (Configuration()..hostApiKey = ''..fiatValue = null) - .buildWidgetUrl(); + final url = + (Configuration() + ..hostApiKey = '' + ..fiatValue = null) + .buildWidgetUrl(); expect(url.queryParameters.containsKey('hostApiKey'), isFalse); expect(url.queryParameters.containsKey('fiatValue'), isFalse); - expect( - url.queryParameters.containsKey('useSendCryptoCallbackVersion'), - isFalse, - ); }); }); - group('RampFlutter events', () { + group('RampFlutter event parsing', () { final widgetUrl = Uri.parse('https://app.rampnetwork.com/'); - test('forwards JSON event maps and ignores non-JSON twins', () { - final events = >[]; - final ramp = RampFlutter.withWidgetUrl(widgetUrl) - ..onWidgetEvent = events.add; + test('ignores non-JSON and unknown types', () { + final events = []; + final ramp = RampFlutter.withWidgetUrl(widgetUrl)..onWidgetEvent = events.add; ramp.handleJavaScriptMessage('not json'); ramp.handleJavaScriptMessage('42'); + ramp.handleJavaScriptMessage(jsonEncode({'type': 'SHARE_LINK'})); + ramp.handleJavaScriptMessage(jsonEncode({'type': 'WIDGET_CONFIG_DONE'})); + + expect(events, isEmpty); + }); + + test('parses original SDK events', () { + final events = []; + final ramp = RampFlutter.withWidgetUrl(widgetUrl)..onWidgetEvent = events.add; + ramp.handleJavaScriptMessage( - '{widgetInstanceId: abc, type: WIDGET_CONFIG_DONE, payload: null}', + jsonEncode({ + 'type': 'PURCHASE_CREATED', + 'payload': { + 'purchase': {'id': 'ignored'}, + }, + }), + ); + ramp.handleJavaScriptMessage( + jsonEncode({ + 'type': 'OFFRAMP_SALE_CREATED', + 'payload': { + 'sale': {'id': 'ignored'}, + }, + }), + ); + ramp.handleJavaScriptMessage( + jsonEncode({ + 'type': 'SEND_CRYPTO', + 'eventVersion': 1, + 'payload': { + 'address': '0xabc', + 'amount': '1', + 'assetInfo': {'chain': 'ETH', 'symbol': 'ETH', 'type': 'ETH'}, + }, + }), ); - ramp.handleJavaScriptMessage(jsonEncode({ - 'type': 'WIDGET_CONFIG_DONE', - 'payload': null, - 'widgetInstanceId': 'abc', - })); - ramp.handleJavaScriptMessage(jsonEncode({ - 'type': 'PURCHASE_CREATED', - 'payload': {'purchase': {'id': 'purchase-id'}}, - })); ramp.handleJavaScriptMessage(jsonEncode({'type': 'CLOSE'})); + ramp.handleJavaScriptMessage(jsonEncode({'type': 'WIDGET_CLOSE'})); expect(events, [ - { - 'type': 'WIDGET_CONFIG_DONE', - 'payload': null, - 'widgetInstanceId': 'abc', - }, - { - 'type': 'PURCHASE_CREATED', - 'payload': { - 'purchase': {'id': 'purchase-id'}, - }, - }, - {'type': 'CLOSE'}, + isA(), + isA(), + isA(), + isA(), + isA(), ]); + final send = events[2] as SendCryptoRequested; + expect(send.payload.address, '0xabc'); + expect(send.payload.assetInfo?.symbol, 'ETH'); + }); + + test('rejects unsupported SEND_CRYPTO eventVersion', () { + final events = []; + final ramp = RampFlutter.withWidgetUrl(widgetUrl)..onWidgetEvent = events.add; + + ramp.handleJavaScriptMessage( + jsonEncode({ + 'type': 'SEND_CRYPTO', + 'eventVersion': 2, + 'payload': {'address': '0xabc', 'amount': '1', 'assetInfo': {}}, + }), + ); + + expect(events, isEmpty); }); }); } From cef13e04b451c19c2f77dd0d58bd145d76c65723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 15:41:21 +0200 Subject: [PATCH 16/56] separate webview --- lib/ramp_flutter.dart | 155 +++----------------------------------- lib/src/ramp_webview.dart | 153 +++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 143 deletions(-) create mode 100644 lib/src/ramp_webview.dart diff --git a/lib/ramp_flutter.dart b/lib/ramp_flutter.dart index c94d1fd..a55a13d 100644 --- a/lib/ramp_flutter.dart +++ b/lib/ramp_flutter.dart @@ -1,161 +1,30 @@ -import 'dart:convert'; - import 'package:flutter/widgets.dart'; import 'package:ramp_flutter/configuration.dart'; +import 'package:ramp_flutter/src/ramp_webview.dart'; import 'package:ramp_flutter/widget_event.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:webview_flutter/webview_flutter.dart'; -import 'package:webview_flutter_android/webview_flutter_android.dart'; -import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; +/// Public API for embedding the Ramp Network widget. class RampFlutter { - RampFlutter(Configuration configuration) : this._(configuration.buildWidgetUrl()); + RampFlutter(Configuration configuration) : _webView = RampWebView(configuration.buildWidgetUrl()); /// Test-only: load a concrete widget URL without going through [Configuration]. @visibleForTesting - RampFlutter.withWidgetUrl(Uri widgetUrl) : this._(widgetUrl); - - RampFlutter._(this._widgetUrl); - - static const _channelName = 'RampInstantMobile'; - - final Uri _widgetUrl; - WebViewController? _webView; - - void Function(WidgetEvent event)? onWidgetEvent; - - Widget get view => WebViewWidget(controller: _ensureWebView()); - - WebViewController _ensureWebView() => _webView ??= _createWebView(); - - WebViewController _createWebView() { - final PlatformWebViewControllerCreationParams params; - if (WebViewPlatform.instance is WebKitWebViewPlatform) { - params = WebKitWebViewControllerCreationParams(allowsInlineMediaPlayback: true); - } else { - params = const PlatformWebViewControllerCreationParams(); - } + RampFlutter.withWidgetUrl(Uri widgetUrl) : _webView = RampWebView(widgetUrl); - debugPrint('RampFlutter: loading $_widgetUrl'); + final RampWebView _webView; - final controller = - WebViewController.fromPlatformCreationParams( - params, - onPermissionRequest: (request) { - final onlyCamera = request.types.every((type) => type == WebViewPermissionResourceType.camera); - if (onlyCamera) { - request.grant(); - } else { - request.deny(); - } - }, - ) - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate( - NavigationDelegate( - onNavigationRequest: (request) { - debugPrint('RampFlutter: allow navigation ${request.url}'); - return NavigationDecision.navigate; - }, - onCreateWindow: (url) { - debugPrint('RampFlutter: open external (create window) $url'); - final uri = Uri.tryParse(url); - if (uri != null) { - _openExternal(uri); - } - }, - onPageStarted: (url) => debugPrint('RampFlutter: page started $url'), - onPageFinished: (url) => debugPrint('RampFlutter: page finished $url'), - onWebResourceError: (error) { - debugPrint( - 'RampFlutter: resource error ' - 'code=${error.errorCode} type=${error.errorType} ' - 'desc=${error.description} url=${error.url}', - ); - }, - onHttpError: (error) { - debugPrint( - 'RampFlutter: HTTP error ' - 'status=${error.response?.statusCode} uri=${error.request?.uri}', - ); - }, - ), - ) - ..addJavaScriptChannel( - _channelName, - onMessageReceived: (message) => handleJavaScriptMessage(message.message), - ); + void Function(WidgetEvent event)? get onWidgetEvent => _webView.onWidgetEvent; - final platform = controller.platform; - if (platform is AndroidWebViewController) { - platform.setMediaPlaybackRequiresUserGesture(false); - } - - controller.loadRequest(_widgetUrl); - return controller; + set onWidgetEvent(void Function(WidgetEvent event)? callback) { + _webView.onWidgetEvent = callback; } - Future _openExternal(Uri uri) async { - try { - if (uri.scheme == 'intent') { - final fallback = _intentFallbackUrl(uri); - if (fallback != null) { - await launchUrl(fallback, mode: LaunchMode.externalApplication); - } - return; - } - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } - } catch (error, stackTrace) { - debugPrint('RampFlutter: failed to open $uri: $error\n$stackTrace'); - } - } + Widget get view => _webView.view; - static Uri? _intentFallbackUrl(Uri intentUri) { - final browserFallback = intentUri.queryParameters['browser_fallback_url']; - if (browserFallback != null && browserFallback.isNotEmpty) { - return Uri.tryParse(browserFallback); - } - return null; - } + Future sendCrypto(String? transactionHash) => _webView.sendCrypto(transactionHash); - Future sendCrypto(String? transactionHash) { - final webView = _webView; - if (webView == null) { - return Future.value(); - } - final message = jsonEncode({ - 'type': 'SEND_CRYPTO_RESULT', - 'eventVersion': 1, - 'payload': {'txHash': transactionHash}, - }); - return webView.runJavaScript('window.postMessage($message, "${_widgetUrl.origin}");'); - } - - void dispose() { - _webView - ?..removeJavaScriptChannel(_channelName) - ..loadRequest(Uri.parse('about:blank')); - _webView = null; - } + void dispose() => _webView.dispose(); @visibleForTesting - void handleJavaScriptMessage(String message) { - // Non-JSON twins from iOS object posts are ignored; keep JSON event maps. - final dynamic decoded; - try { - decoded = jsonDecode(message); - } on FormatException { - return; - } - if (decoded is! Map) { - return; - } - final event = WidgetEvent.tryParse(Map.from(decoded)); - if (event == null) { - return; - } - onWidgetEvent?.call(event); - } + void handleJavaScriptMessage(String message) => _webView.handleJavaScriptMessage(message); } diff --git a/lib/src/ramp_webview.dart b/lib/src/ramp_webview.dart new file mode 100644 index 0000000..1772786 --- /dev/null +++ b/lib/src/ramp_webview.dart @@ -0,0 +1,153 @@ +import 'dart:convert'; + +import 'package:flutter/widgets.dart'; +import 'package:ramp_flutter/widget_event.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_android/webview_flutter_android.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; + +/// Owns the widget WebView. Not part of the public SDK surface. +class RampWebView { + RampWebView(this._widgetUrl); + + static const _channelName = 'RampInstantMobile'; + + final Uri _widgetUrl; + WebViewController? _controller; + + void Function(WidgetEvent event)? onWidgetEvent; + + Widget get view => WebViewWidget(controller: _ensureController()); + + WebViewController _ensureController() => _controller ??= _createController(); + + WebViewController _createController() { + final PlatformWebViewControllerCreationParams params; + if (WebViewPlatform.instance is WebKitWebViewPlatform) { + params = WebKitWebViewControllerCreationParams(allowsInlineMediaPlayback: true); + } else { + params = const PlatformWebViewControllerCreationParams(); + } + + debugPrint('RampFlutter: loading $_widgetUrl'); + + final controller = + WebViewController.fromPlatformCreationParams( + params, + onPermissionRequest: (request) { + final onlyCamera = request.types.every((type) => type == WebViewPermissionResourceType.camera); + if (onlyCamera) { + request.grant(); + } else { + request.deny(); + } + }, + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onNavigationRequest: (request) { + debugPrint('RampFlutter: allow navigation ${request.url}'); + return NavigationDecision.navigate; + }, + onCreateWindow: (url) { + debugPrint('RampFlutter: open external (create window) $url'); + final uri = Uri.tryParse(url); + if (uri != null) { + _openExternal(uri); + } + }, + onPageStarted: (url) => debugPrint('RampFlutter: page started $url'), + onPageFinished: (url) => debugPrint('RampFlutter: page finished $url'), + onWebResourceError: (error) { + debugPrint( + 'RampFlutter: resource error ' + 'code=${error.errorCode} type=${error.errorType} ' + 'desc=${error.description} url=${error.url}', + ); + }, + onHttpError: (error) { + debugPrint( + 'RampFlutter: HTTP error ' + 'status=${error.response?.statusCode} uri=${error.request?.uri}', + ); + }, + ), + ) + ..addJavaScriptChannel( + _channelName, + onMessageReceived: (message) => handleJavaScriptMessage(message.message), + ); + + final platform = controller.platform; + if (platform is AndroidWebViewController) { + platform.setMediaPlaybackRequiresUserGesture(false); + } + + controller.loadRequest(_widgetUrl); + return controller; + } + + Future _openExternal(Uri uri) async { + try { + if (uri.scheme == 'intent') { + final fallback = _intentFallbackUrl(uri); + if (fallback != null) { + await launchUrl(fallback, mode: LaunchMode.externalApplication); + } + return; + } + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } catch (error, stackTrace) { + debugPrint('RampFlutter: failed to open $uri: $error\n$stackTrace'); + } + } + + static Uri? _intentFallbackUrl(Uri intentUri) { + final browserFallback = intentUri.queryParameters['browser_fallback_url']; + if (browserFallback != null && browserFallback.isNotEmpty) { + return Uri.tryParse(browserFallback); + } + return null; + } + + Future sendCrypto(String? transactionHash) { + final webView = _controller; + if (webView == null) { + return Future.value(); + } + final message = jsonEncode({ + 'type': 'SEND_CRYPTO_RESULT', + 'eventVersion': 1, + 'payload': {'txHash': transactionHash}, + }); + return webView.runJavaScript('window.postMessage($message, "${_widgetUrl.origin}");'); + } + + void dispose() { + _controller + ?..removeJavaScriptChannel(_channelName) + ..loadRequest(Uri.parse('about:blank')); + _controller = null; + } + + void handleJavaScriptMessage(String message) { + final dynamic decoded; + try { + decoded = jsonDecode(message); + } on FormatException { + return; + } + if (decoded is! Map) { + return; + } + final event = WidgetEvent.tryParse(Map.from(decoded)); + if (event == null) { + return; + } + onWidgetEvent?.call(event); + } +} From fa7d9d72baf60195638b749efc73598439888abd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 15:54:00 +0200 Subject: [PATCH 17/56] update events --- CHANGELOG.md | 7 +- README.md | 15 ++- example/lib/main.dart | 28 ++++- lib/events/asset_info.dart | 38 +++++++ lib/events/json_map.dart | 37 ++++++ lib/events/offramp_sale_created.dart | 104 ++++++++++++++++- lib/events/purchase_created.dart | 97 +++++++++++++++- lib/events/ramp_closed.dart | 5 - lib/events/request_crypto_account.dart | 25 ++++ lib/events/send_crypto_requested.dart | 32 +----- lib/events/widget_close.dart | 28 +++++ lib/events/widget_config_done.dart | 5 + lib/events/widget_config_failed.dart | 5 + lib/host_event.dart | 13 +++ .../request_crypto_account_result.dart | 28 +++++ lib/host_events/send_crypto_result.dart | 17 +++ lib/ramp_flutter.dart | 5 + lib/src/ramp_webview.dart | 12 +- lib/widget_event.dart | 50 ++++++-- test/ramp_webview_test.dart | 107 ++++++++++++++++-- 20 files changed, 586 insertions(+), 72 deletions(-) create mode 100644 lib/events/asset_info.dart delete mode 100644 lib/events/ramp_closed.dart create mode 100644 lib/events/request_crypto_account.dart create mode 100644 lib/events/widget_close.dart create mode 100644 lib/events/widget_config_done.dart create mode 100644 lib/events/widget_config_failed.dart create mode 100644 lib/host_event.dart create mode 100644 lib/host_events/request_crypto_account_result.dart create mode 100644 lib/host_events/send_crypto_result.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index f1b2b0c..9ca8141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,10 @@ Android/iOS plugin shells). * Breaking: SDK no longer presents UI. Create `RampFlutter(configuration)`, embed `ramp.view` in your own route/sheet, and call `dispose` when done. -* Widget JS events match the original Flutter SDK surface: `PurchaseCreated`, - `OfframpSaleCreated`, `SendCryptoRequested`, and `RampClosed` - (`WIDGET_CLOSE` / `CLOSE`). Purchase and sale are signals only. +* Widget → host events: `WidgetConfigDone` / `Failed`, `PurchaseCreated`, + `OfframpSaleCreated`, `SendCryptoRequested`, `RequestCryptoAccount`, + `WidgetClose`. Host → widget: `SendCryptoResult` (`sendCrypto` convenience) + and `RequestCryptoAccountResult` via `postHostEvent`. * Add `Configuration.offrampAsset` and build the widget URL in Dart (`Configuration.buildWidgetUrl`), including `sdkType` / `sdkVersion` / `variant`. diff --git a/README.md b/README.md index e3d7338..b0e03f3 100644 --- a/README.md +++ b/README.md @@ -32,14 +32,23 @@ sheet, dialog, etc.) and must call `dispose` when it is dismissed. final ramp = RampFlutter(configuration) ..onWidgetEvent = (event) { switch (event) { - case PurchaseCreated(): + case PurchaseCreated(:final payload): + // payload.purchase, purchaseViewToken, apiUrl break; - case OfframpSaleCreated(): + case OfframpSaleCreated(:final payload): break; case SendCryptoRequested(:final payload): ramp.sendCrypto(txHash); - case RampClosed(): + // or: ramp.postHostEvent(SendCryptoResult.txHash(txHash)); + case RequestCryptoAccount(:final payload): + ramp.postHostEvent( + RequestCryptoAccountResult.account(address: userAddress), + ); + case WidgetClose(): Navigator.of(context).pop(); + case WidgetConfigDone(): + case WidgetConfigFailed(): + break; } }; diff --git a/example/lib/main.dart b/example/lib/main.dart index 2a6b092..7ef27ad 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:ramp_flutter/configuration.dart'; +import 'package:ramp_flutter/host_event.dart'; import 'package:ramp_flutter/ramp_flutter.dart'; import 'package:ramp_flutter/widget_event.dart'; @@ -79,10 +80,17 @@ class _RampFlutterAppState extends State { ramp.onWidgetEvent = (event) { switch (event) { - case PurchaseCreated(): - _addDebugEvent('PURCHASE_CREATED'); - case OfframpSaleCreated(): - _addDebugEvent('OFFRAMP_SALE_CREATED'); + case WidgetConfigDone(): + _addDebugEvent('WIDGET_CONFIG_DONE'); + case WidgetConfigFailed(): + _addDebugEvent('WIDGET_CONFIG_FAILED'); + case PurchaseCreated(:final payload): + _addDebugEvent('PURCHASE_CREATED', { + 'id': payload.purchase?.id, + 'asset': payload.purchase?.asset?.symbol, + }); + case OfframpSaleCreated(:final payload): + _addDebugEvent('OFFRAMP_SALE_CREATED', {'id': payload.sale?.id}); case SendCryptoRequested(:final payload): _addDebugEvent('SEND_CRYPTO', { 'address': payload.address, @@ -90,8 +98,16 @@ class _RampFlutterAppState extends State { 'asset': payload.assetInfo?.symbol, }); ramp.sendCrypto('123'); - case RampClosed(): - _addDebugEvent('CLOSE'); + case RequestCryptoAccount(:final payload): + _addDebugEvent('REQUEST_CRYPTO_ACCOUNT', { + 'type': payload.type, + 'assetSymbol': payload.assetSymbol, + }); + ramp.postHostEvent( + RequestCryptoAccountResult.account(address: '0xabc', type: payload.type, assetSymbol: payload.assetSymbol), + ); + case WidgetClose(:final payload): + _addDebugEvent('WIDGET_CLOSE', {'showAlert': payload.showAlert}); } }; diff --git a/lib/events/asset_info.dart b/lib/events/asset_info.dart new file mode 100644 index 0000000..6f1f9a5 --- /dev/null +++ b/lib/events/asset_info.dart @@ -0,0 +1,38 @@ +part of '../widget_event.dart'; + +/// Asset metadata shared by purchase / send-crypto / sale payloads. +class AssetInfo { + const AssetInfo({ + this.uai, + this.address, + this.symbol, + this.chain, + this.type, + this.name, + this.decimals, + }); + + /// UAI is undefined for assets that don't have a SLIP-44 coin ID. + final String? uai; + final String? address; + final String? symbol; + final String? chain; + final String? type; + final String? name; + final int? decimals; + + factory AssetInfo.fromJson(Map? json) { + if (json == null) { + return const AssetInfo(); + } + return AssetInfo( + uai: json['uai'] as String?, + address: json['address'] as String?, + symbol: json['symbol'] as String?, + chain: json['chain'] as String?, + type: json['type'] as String?, + name: json['name'] as String?, + decimals: asInt(json['decimals']), + ); + } +} diff --git a/lib/events/json_map.dart b/lib/events/json_map.dart index f04628d..965646c 100644 --- a/lib/events/json_map.dart +++ b/lib/events/json_map.dart @@ -7,3 +7,40 @@ Map? asStringKeyedMap(dynamic value) { } return null; } + +String? asString(dynamic value) { + if (value == null) { + return null; + } + if (value is String) { + return value; + } + return value.toString(); +} + +int? asInt(dynamic value) { + if (value is int) { + return value; + } + if (value is num) { + return value.toInt(); + } + return null; +} + +double? asDouble(dynamic value) { + if (value is double) { + return value; + } + if (value is num) { + return value.toDouble(); + } + return null; +} + +bool? asBool(dynamic value) { + if (value is bool) { + return value; + } + return null; +} diff --git a/lib/events/offramp_sale_created.dart b/lib/events/offramp_sale_created.dart index 8139af2..72c2db9 100644 --- a/lib/events/offramp_sale_created.dart +++ b/lib/events/offramp_sale_created.dart @@ -1,5 +1,107 @@ part of '../widget_event.dart'; final class OfframpSaleCreated extends WidgetEvent { - const OfframpSaleCreated(); + const OfframpSaleCreated(this.payload, {super.widgetInstanceId}); + + final OfframpSaleCreatedPayload payload; +} + +class OfframpSaleCreatedPayload { + const OfframpSaleCreatedPayload({this.sale, this.saleViewToken, this.apiUrl}); + + final SaleDetails? sale; + final String? saleViewToken; + final String? apiUrl; + + factory OfframpSaleCreatedPayload.fromJson(Map? json) { + if (json == null) { + return const OfframpSaleCreatedPayload(); + } + return OfframpSaleCreatedPayload( + sale: SaleDetails.fromJson(asStringKeyedMap(json['sale'])), + saleViewToken: json['saleViewToken'] as String?, + apiUrl: json['apiUrl'] as String?, + ); + } +} + +class SaleDetails { + const SaleDetails({this.id, this.createdAt, this.updatedAt, this.crypto, this.fiat, this.fees, this.exchangeRate}); + + final String? id; + final String? createdAt; + final String? updatedAt; + final SaleCrypto? crypto; + final SaleFiat? fiat; + final SaleFees? fees; + final String? exchangeRate; + + factory SaleDetails.fromJson(Map? json) { + if (json == null) { + return const SaleDetails(); + } + return SaleDetails( + id: json['id'] as String?, + createdAt: json['createdAt'] as String?, + updatedAt: json['updatedAt'] as String?, + crypto: SaleCrypto.fromJson(asStringKeyedMap(json['crypto'])), + fiat: SaleFiat.fromJson(asStringKeyedMap(json['fiat'])), + fees: SaleFees.fromJson(asStringKeyedMap(json['fees'])), + exchangeRate: asString(json['exchangeRate']), + ); + } +} + +class SaleCrypto { + const SaleCrypto({this.amount, this.status, this.assetInfo}); + + final String? amount; + final String? status; + final AssetInfo? assetInfo; + + factory SaleCrypto.fromJson(Map? json) { + if (json == null) { + return const SaleCrypto(); + } + return SaleCrypto( + amount: asString(json['amount']), + status: json['status'] as String?, + assetInfo: AssetInfo.fromJson(asStringKeyedMap(json['assetInfo'])), + ); + } +} + +class SaleFiat { + const SaleFiat({this.amount, this.currencySymbol, this.status, this.payoutMethod}); + + final String? amount; + final String? currencySymbol; + final String? status; + final String? payoutMethod; + + factory SaleFiat.fromJson(Map? json) { + if (json == null) { + return const SaleFiat(); + } + return SaleFiat( + amount: asString(json['amount']), + currencySymbol: asString(json['currencySymbol']), + status: json['status'] as String?, + payoutMethod: json['payoutMethod'] as String?, + ); + } +} + +class SaleFees { + const SaleFees({this.amount, this.currencySymbol}); + + final String? amount; + final String? currencySymbol; + + factory SaleFees.fromJson(Map? json) { + if (json == null) { + return const SaleFees(); + } + return SaleFees(amount: asString(json['amount']), currencySymbol: asString(json['currencySymbol'])); + } } diff --git a/lib/events/purchase_created.dart b/lib/events/purchase_created.dart index 3b1b365..6a1612a 100644 --- a/lib/events/purchase_created.dart +++ b/lib/events/purchase_created.dart @@ -1,5 +1,100 @@ part of '../widget_event.dart'; final class PurchaseCreated extends WidgetEvent { - const PurchaseCreated(); + const PurchaseCreated(this.payload, {super.widgetInstanceId}); + + final PurchaseCreatedPayload payload; +} + +class PurchaseCreatedPayload { + const PurchaseCreatedPayload({this.purchase, this.purchaseViewToken, this.apiUrl}); + + final PurchaseDetails? purchase; + final String? purchaseViewToken; + final String? apiUrl; + + factory PurchaseCreatedPayload.fromJson(Map? json) { + if (json == null) { + return const PurchaseCreatedPayload(); + } + return PurchaseCreatedPayload( + purchase: PurchaseDetails.fromJson(asStringKeyedMap(json['purchase'])), + purchaseViewToken: json['purchaseViewToken'] as String?, + apiUrl: json['apiUrl'] as String?, + ); + } +} + +class PurchaseDetails { + const PurchaseDetails({ + this.id, + this.endTime, + this.asset, + this.receiverAddress, + this.cryptoAmount, + this.fiatCurrency, + this.fiatValue, + this.assetExchangeRate, + this.assetExchangeRateEur, + this.fiatExchangeRateEur, + this.baseRampFee, + this.networkFee, + this.appliedFee, + this.paymentMethodType, + this.finalTxHash, + this.createdAt, + this.updatedAt, + this.status, + this.purchaseViewToken, + this.hostFeeCut, + }); + + final String? id; + final String? endTime; + final AssetInfo? asset; + final String? receiverAddress; + final String? cryptoAmount; + final String? fiatCurrency; + final double? fiatValue; + final double? assetExchangeRate; + final double? assetExchangeRateEur; + final double? fiatExchangeRateEur; + final double? baseRampFee; + final double? networkFee; + final double? appliedFee; + final String? paymentMethodType; + final String? finalTxHash; + final String? createdAt; + final String? updatedAt; + final String? status; + final String? purchaseViewToken; + final double? hostFeeCut; + + factory PurchaseDetails.fromJson(Map? json) { + if (json == null) { + return const PurchaseDetails(); + } + return PurchaseDetails( + id: json['id'] as String?, + endTime: json['endTime'] as String?, + asset: AssetInfo.fromJson(asStringKeyedMap(json['asset'])), + receiverAddress: json['receiverAddress'] as String?, + cryptoAmount: asString(json['cryptoAmount']), + fiatCurrency: json['fiatCurrency'] as String?, + fiatValue: asDouble(json['fiatValue']), + assetExchangeRate: asDouble(json['assetExchangeRate']), + assetExchangeRateEur: asDouble(json['assetExchangeRateEur']), + fiatExchangeRateEur: asDouble(json['fiatExchangeRateEur']), + baseRampFee: asDouble(json['baseRampFee']), + networkFee: asDouble(json['networkFee']), + appliedFee: asDouble(json['appliedFee']), + paymentMethodType: json['paymentMethodType'] as String?, + finalTxHash: json['finalTxHash'] as String?, + createdAt: json['createdAt'] as String?, + updatedAt: json['updatedAt'] as String?, + status: json['status'] as String?, + purchaseViewToken: json['purchaseViewToken'] as String?, + hostFeeCut: asDouble(json['hostFeeCut']), + ); + } } diff --git a/lib/events/ramp_closed.dart b/lib/events/ramp_closed.dart deleted file mode 100644 index c33ae60..0000000 --- a/lib/events/ramp_closed.dart +++ /dev/null @@ -1,5 +0,0 @@ -part of '../widget_event.dart'; - -final class RampClosed extends WidgetEvent { - const RampClosed(); -} diff --git a/lib/events/request_crypto_account.dart b/lib/events/request_crypto_account.dart new file mode 100644 index 0000000..7dfbcc1 --- /dev/null +++ b/lib/events/request_crypto_account.dart @@ -0,0 +1,25 @@ +part of '../widget_event.dart'; + +final class RequestCryptoAccount extends WidgetEvent { + const RequestCryptoAccount(this.payload, {super.widgetInstanceId}); + + final RequestCryptoAccountPayload payload; +} + +class RequestCryptoAccountPayload { + const RequestCryptoAccountPayload({this.type, this.assetSymbol}); + + /// Ramp's internal chain id (e.g. `"ETH"`, `"POLYGON"`). + final String? type; + final String? assetSymbol; + + factory RequestCryptoAccountPayload.fromJson(Map? json) { + if (json == null) { + return const RequestCryptoAccountPayload(); + } + return RequestCryptoAccountPayload( + type: json['type'] as String?, + assetSymbol: json['assetSymbol'] as String?, + ); + } +} diff --git a/lib/events/send_crypto_requested.dart b/lib/events/send_crypto_requested.dart index 6987e72..19e2cb9 100644 --- a/lib/events/send_crypto_requested.dart +++ b/lib/events/send_crypto_requested.dart @@ -1,7 +1,8 @@ part of '../widget_event.dart'; final class SendCryptoRequested extends WidgetEvent { - const SendCryptoRequested(this.payload); + const SendCryptoRequested(this.payload, {super.widgetInstanceId}); + final SendCryptoPayload payload; } @@ -10,7 +11,7 @@ class SendCryptoPayload { final String? address; final String? amount; - final SendCryptoAssetInfo? assetInfo; + final AssetInfo? assetInfo; factory SendCryptoPayload.fromJson(Map? json) { if (json == null) { @@ -18,31 +19,8 @@ class SendCryptoPayload { } return SendCryptoPayload( address: json['address'] as String?, - amount: json['amount'] as String?, - assetInfo: SendCryptoAssetInfo.fromJson(asStringKeyedMap(json['assetInfo'])), - ); - } -} - -class SendCryptoAssetInfo { - const SendCryptoAssetInfo({this.chain, this.decimals, this.name, this.symbol, this.type}); - - final String? chain; - final int? decimals; - final String? name; - final String? symbol; - final String? type; - - factory SendCryptoAssetInfo.fromJson(Map? json) { - if (json == null) { - return const SendCryptoAssetInfo(); - } - return SendCryptoAssetInfo( - chain: json['chain'] as String?, - decimals: (json['decimals'] as num?)?.toInt(), - name: json['name'] as String?, - symbol: json['symbol'] as String?, - type: json['type'] as String?, + amount: asString(json['amount']), + assetInfo: AssetInfo.fromJson(asStringKeyedMap(json['assetInfo'])), ); } } diff --git a/lib/events/widget_close.dart b/lib/events/widget_close.dart new file mode 100644 index 0000000..cfc5f23 --- /dev/null +++ b/lib/events/widget_close.dart @@ -0,0 +1,28 @@ +part of '../widget_event.dart'; + +final class WidgetClose extends WidgetEvent { + const WidgetClose({this.payload = const WidgetClosePayload(), super.widgetInstanceId}); + + final WidgetClosePayload payload; +} + +class WidgetClosePayload { + const WidgetClosePayload({this.showAlert, this.descriptionText, this.acceptText, this.rejectText}); + + final bool? showAlert; + final String? descriptionText; + final String? acceptText; + final String? rejectText; + + factory WidgetClosePayload.fromJson(Map? json) { + if (json == null) { + return const WidgetClosePayload(); + } + return WidgetClosePayload( + showAlert: asBool(json['showAlert']), + descriptionText: json['descriptionText'] as String?, + acceptText: json['acceptText'] as String?, + rejectText: json['rejectText'] as String?, + ); + } +} diff --git a/lib/events/widget_config_done.dart b/lib/events/widget_config_done.dart new file mode 100644 index 0000000..c35ee60 --- /dev/null +++ b/lib/events/widget_config_done.dart @@ -0,0 +1,5 @@ +part of '../widget_event.dart'; + +final class WidgetConfigDone extends WidgetEvent { + const WidgetConfigDone({super.widgetInstanceId}); +} diff --git a/lib/events/widget_config_failed.dart b/lib/events/widget_config_failed.dart new file mode 100644 index 0000000..9afb0aa --- /dev/null +++ b/lib/events/widget_config_failed.dart @@ -0,0 +1,5 @@ +part of '../widget_event.dart'; + +final class WidgetConfigFailed extends WidgetEvent { + const WidgetConfigFailed({super.widgetInstanceId}); +} diff --git a/lib/host_event.dart b/lib/host_event.dart new file mode 100644 index 0000000..bc575d6 --- /dev/null +++ b/lib/host_event.dart @@ -0,0 +1,13 @@ +part 'host_events/request_crypto_account_result.dart'; +part 'host_events/send_crypto_result.dart'; + +/// Outgoing host → widget events for the Flutter SDK surface. +sealed class HostEvent { + const HostEvent(); + + String get type; + + Object? get jsonPayload; + + Map toJson() => {'type': type, 'payload': ?jsonPayload}; +} diff --git a/lib/host_events/request_crypto_account_result.dart b/lib/host_events/request_crypto_account_result.dart new file mode 100644 index 0000000..7543df5 --- /dev/null +++ b/lib/host_events/request_crypto_account_result.dart @@ -0,0 +1,28 @@ +part of '../host_event.dart'; + +final class RequestCryptoAccountResult extends HostEvent { + const RequestCryptoAccountResult._(this.payload); + + factory RequestCryptoAccountResult.account({ + required String address, + String? type, + String? name, + String? assetSymbol, + }) => RequestCryptoAccountResult._({ + 'address': address, + 'type': ?type, + 'name': ?name, + 'assetSymbol': ?assetSymbol, + }); + + factory RequestCryptoAccountResult.error([String? error]) => + RequestCryptoAccountResult._({'error': error}); + + final Map payload; + + @override + String get type => 'REQUEST_CRYPTO_ACCOUNT_RESULT'; + + @override + Object? get jsonPayload => payload; +} diff --git a/lib/host_events/send_crypto_result.dart b/lib/host_events/send_crypto_result.dart new file mode 100644 index 0000000..08be6e3 --- /dev/null +++ b/lib/host_events/send_crypto_result.dart @@ -0,0 +1,17 @@ +part of '../host_event.dart'; + +final class SendCryptoResult extends HostEvent { + const SendCryptoResult._(this.payload); + + factory SendCryptoResult.txHash(String? txHash) => SendCryptoResult._({'txHash': txHash}); + + factory SendCryptoResult.error([String? error]) => SendCryptoResult._({'error': error}); + + final Map payload; + + @override + String get type => 'SEND_CRYPTO_RESULT'; + + @override + Object? get jsonPayload => payload; +} diff --git a/lib/ramp_flutter.dart b/lib/ramp_flutter.dart index a55a13d..611b405 100644 --- a/lib/ramp_flutter.dart +++ b/lib/ramp_flutter.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:ramp_flutter/configuration.dart'; +import 'package:ramp_flutter/host_event.dart'; import 'package:ramp_flutter/src/ramp_webview.dart'; import 'package:ramp_flutter/widget_event.dart'; @@ -21,6 +22,10 @@ class RampFlutter { Widget get view => _webView.view; + /// Posts a host → widget event (`HostEventTypes`). + Future postHostEvent(HostEvent event) => _webView.postHostEvent(event); + + /// Convenience for [SendCryptoResult.txHash]. Future sendCrypto(String? transactionHash) => _webView.sendCrypto(transactionHash); void dispose() => _webView.dispose(); diff --git a/lib/src/ramp_webview.dart b/lib/src/ramp_webview.dart index 1772786..a4700d3 100644 --- a/lib/src/ramp_webview.dart +++ b/lib/src/ramp_webview.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:flutter/widgets.dart'; +import 'package:ramp_flutter/host_event.dart'; import 'package:ramp_flutter/widget_event.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -114,19 +115,18 @@ class RampWebView { return null; } - Future sendCrypto(String? transactionHash) { + Future postHostEvent(HostEvent event) { final webView = _controller; if (webView == null) { return Future.value(); } - final message = jsonEncode({ - 'type': 'SEND_CRYPTO_RESULT', - 'eventVersion': 1, - 'payload': {'txHash': transactionHash}, - }); + final message = jsonEncode(event.toJson()); return webView.runJavaScript('window.postMessage($message, "${_widgetUrl.origin}");'); } + Future sendCrypto(String? transactionHash) => + postHostEvent(SendCryptoResult.txHash(transactionHash)); + void dispose() { _controller ?..removeJavaScriptChannel(_channelName) diff --git a/lib/widget_event.dart b/lib/widget_event.dart index f55d012..e42c91f 100644 --- a/lib/widget_event.dart +++ b/lib/widget_event.dart @@ -1,16 +1,19 @@ import 'package:ramp_flutter/events/json_map.dart'; +part 'events/asset_info.dart'; part 'events/offramp_sale_created.dart'; part 'events/purchase_created.dart'; -part 'events/ramp_closed.dart'; +part 'events/request_crypto_account.dart'; part 'events/send_crypto_requested.dart'; +part 'events/widget_close.dart'; +part 'events/widget_config_done.dart'; +part 'events/widget_config_failed.dart'; -/// Incoming widget events from the original Flutter SDK surface. -/// -/// `PURCHASE_CREATED` / `OFFRAMP_SALE_CREATED` / `WIDGET_CLOSE` are signals -/// only (no payload decoding). +/// Incoming widget → host events for the Flutter SDK surface. sealed class WidgetEvent { - const WidgetEvent(); + const WidgetEvent({this.widgetInstanceId}); + + final String? widgetInstanceId; static WidgetEvent? tryParse(Map json) { final type = json['type']; @@ -18,13 +21,24 @@ sealed class WidgetEvent { return null; } final payload = asStringKeyedMap(json['payload']); + final widgetInstanceId = json['widgetInstanceId'] as String?; try { switch (type) { - case 'OFFRAMP_SALE_CREATED': - return const OfframpSaleCreated(); + case 'WIDGET_CONFIG_DONE': + return WidgetConfigDone(widgetInstanceId: widgetInstanceId); + case 'WIDGET_CONFIG_FAILED': + return WidgetConfigFailed(widgetInstanceId: widgetInstanceId); case 'PURCHASE_CREATED': - return const PurchaseCreated(); + return PurchaseCreated( + PurchaseCreatedPayload.fromJson(payload), + widgetInstanceId: widgetInstanceId, + ); + case 'OFFRAMP_SALE_CREATED': + return OfframpSaleCreated( + OfframpSaleCreatedPayload.fromJson(payload), + widgetInstanceId: widgetInstanceId, + ); case 'SEND_CRYPTO': final version = json['eventVersion']; if (version != null && version != 1) { @@ -33,10 +47,24 @@ sealed class WidgetEvent { if (payload == null) { return null; } - return SendCryptoRequested(SendCryptoPayload.fromJson(payload)); + return SendCryptoRequested( + SendCryptoPayload.fromJson(payload), + widgetInstanceId: widgetInstanceId, + ); + case 'REQUEST_CRYPTO_ACCOUNT': + if (payload == null) { + return null; + } + return RequestCryptoAccount( + RequestCryptoAccountPayload.fromJson(payload), + widgetInstanceId: widgetInstanceId, + ); case 'WIDGET_CLOSE': case 'CLOSE': - return const RampClosed(); + return WidgetClose( + payload: WidgetClosePayload.fromJson(payload), + widgetInstanceId: widgetInstanceId, + ); default: return null; } diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index e206a53..1cf6aa7 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:ramp_flutter/configuration.dart'; +import 'package:ramp_flutter/host_event.dart'; import 'package:ramp_flutter/ramp_flutter.dart'; import 'package:ramp_flutter/widget_event.dart'; @@ -61,20 +62,30 @@ void main() { ramp.handleJavaScriptMessage('not json'); ramp.handleJavaScriptMessage('42'); ramp.handleJavaScriptMessage(jsonEncode({'type': 'SHARE_LINK'})); - ramp.handleJavaScriptMessage(jsonEncode({'type': 'WIDGET_CONFIG_DONE'})); expect(events, isEmpty); }); - test('parses original SDK events', () { + test('parses supported widget events', () { final events = []; final ramp = RampFlutter.withWidgetUrl(widgetUrl)..onWidgetEvent = events.add; + ramp.handleJavaScriptMessage( + jsonEncode({'type': 'WIDGET_CONFIG_DONE', 'payload': null, 'widgetInstanceId': 'w1'}), + ); + ramp.handleJavaScriptMessage(jsonEncode({'type': 'WIDGET_CONFIG_FAILED', 'payload': null})); ramp.handleJavaScriptMessage( jsonEncode({ 'type': 'PURCHASE_CREATED', 'payload': { - 'purchase': {'id': 'ignored'}, + 'purchase': { + 'id': 'p1', + 'asset': {'symbol': 'ETH', 'chain': 'ETH', 'type': 'NATIVE', 'name': 'Ether', 'decimals': 18}, + 'cryptoAmount': '100', + 'fiatValue': 50.5, + }, + 'purchaseViewToken': 'token', + 'apiUrl': 'https://api.example.com', }, }), ); @@ -82,7 +93,19 @@ void main() { jsonEncode({ 'type': 'OFFRAMP_SALE_CREATED', 'payload': { - 'sale': {'id': 'ignored'}, + 'sale': { + 'id': 's1', + 'crypto': { + 'amount': '1', + 'status': 'RECEIVED', + 'assetInfo': {'symbol': 'ETH'}, + }, + 'fiat': {'amount': '10', 'currencySymbol': 'EUR', 'status': 'initiated'}, + 'fees': {'amount': '1', 'currencySymbol': 978}, + 'exchangeRate': '10', + }, + 'saleViewToken': 'sale-token', + 'apiUrl': 'https://api.example.com', }, }), ); @@ -93,23 +116,68 @@ void main() { 'payload': { 'address': '0xabc', 'amount': '1', - 'assetInfo': {'chain': 'ETH', 'symbol': 'ETH', 'type': 'ETH'}, + 'assetInfo': { + 'uai': 'eip155:1/slip44:60', + 'address': null, + 'chain': 'ETH', + 'symbol': 'ETH', + 'type': 'NATIVE', + 'name': 'Ether', + 'decimals': 18, + }, }, }), ); + ramp.handleJavaScriptMessage( + jsonEncode({ + 'type': 'REQUEST_CRYPTO_ACCOUNT', + 'payload': {'type': 'ETH', 'assetSymbol': 'ETH'}, + }), + ); + ramp.handleJavaScriptMessage( + jsonEncode({ + 'type': 'WIDGET_CLOSE', + 'payload': {'showAlert': true, 'descriptionText': 'Leave?'}, + }), + ); ramp.handleJavaScriptMessage(jsonEncode({'type': 'CLOSE'})); - ramp.handleJavaScriptMessage(jsonEncode({'type': 'WIDGET_CLOSE'})); expect(events, [ + isA(), + isA(), isA(), isA(), isA(), - isA(), - isA(), + isA(), + isA(), + isA(), ]); - final send = events[2] as SendCryptoRequested; + + expect(events[0].widgetInstanceId, 'w1'); + + final purchase = events[2] as PurchaseCreated; + expect(purchase.payload.purchase?.id, 'p1'); + expect(purchase.payload.purchase?.asset?.symbol, 'ETH'); + expect(purchase.payload.purchase?.fiatValue, 50.5); + expect(purchase.payload.purchaseViewToken, 'token'); + + final sale = events[3] as OfframpSaleCreated; + expect(sale.payload.sale?.id, 's1'); + expect(sale.payload.sale?.fees?.currencySymbol, '978'); + expect(sale.payload.saleViewToken, 'sale-token'); + + final send = events[4] as SendCryptoRequested; expect(send.payload.address, '0xabc'); + expect(send.payload.assetInfo?.uai, 'eip155:1/slip44:60'); expect(send.payload.assetInfo?.symbol, 'ETH'); + + final account = events[5] as RequestCryptoAccount; + expect(account.payload.type, 'ETH'); + expect(account.payload.assetSymbol, 'ETH'); + + final close = events[6] as WidgetClose; + expect(close.payload.showAlert, isTrue); + expect(close.payload.descriptionText, 'Leave?'); }); test('rejects unsupported SEND_CRYPTO eventVersion', () { @@ -127,4 +195,25 @@ void main() { expect(events, isEmpty); }); }); + + group('HostEvent serialization', () { + test('encodes SendCryptoResult and RequestCryptoAccountResult', () { + expect(SendCryptoResult.txHash('0xhash').toJson(), { + 'type': 'SEND_CRYPTO_RESULT', + 'payload': {'txHash': '0xhash'}, + }); + expect(SendCryptoResult.error('failed').toJson(), { + 'type': 'SEND_CRYPTO_RESULT', + 'payload': {'error': 'failed'}, + }); + expect(RequestCryptoAccountResult.account(address: '0xabc', type: 'ETH').toJson(), { + 'type': 'REQUEST_CRYPTO_ACCOUNT_RESULT', + 'payload': {'address': '0xabc', 'type': 'ETH'}, + }); + expect(RequestCryptoAccountResult.error('denied').toJson(), { + 'type': 'REQUEST_CRYPTO_ACCOUNT_RESULT', + 'payload': {'error': 'denied'}, + }); + }); + }); } From 8f1cc9af2d7baff204c8f61159536b763fb9d7d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 16:05:34 +0200 Subject: [PATCH 18/56] extract models --- lib/events/offramp_sale_created.dart | 81 ------------------------- lib/events/purchase_created.dart | 74 ----------------------- lib/events/request_crypto_account.dart | 1 - lib/host_event.dart | 1 - lib/{events => models}/asset_info.dart | 4 +- lib/{events => models}/json_map.dart | 0 lib/models/purchase_details.dart | 76 +++++++++++++++++++++++ lib/models/sale_details.dart | 83 ++++++++++++++++++++++++++ lib/ramp_flutter.dart | 3 - lib/src/ramp_webview.dart | 1 - lib/widget_event.dart | 11 +++- 11 files changed, 168 insertions(+), 167 deletions(-) rename lib/{events => models}/asset_info.dart (81%) rename lib/{events => models}/json_map.dart (100%) create mode 100644 lib/models/purchase_details.dart create mode 100644 lib/models/sale_details.dart diff --git a/lib/events/offramp_sale_created.dart b/lib/events/offramp_sale_created.dart index 72c2db9..293284a 100644 --- a/lib/events/offramp_sale_created.dart +++ b/lib/events/offramp_sale_created.dart @@ -24,84 +24,3 @@ class OfframpSaleCreatedPayload { ); } } - -class SaleDetails { - const SaleDetails({this.id, this.createdAt, this.updatedAt, this.crypto, this.fiat, this.fees, this.exchangeRate}); - - final String? id; - final String? createdAt; - final String? updatedAt; - final SaleCrypto? crypto; - final SaleFiat? fiat; - final SaleFees? fees; - final String? exchangeRate; - - factory SaleDetails.fromJson(Map? json) { - if (json == null) { - return const SaleDetails(); - } - return SaleDetails( - id: json['id'] as String?, - createdAt: json['createdAt'] as String?, - updatedAt: json['updatedAt'] as String?, - crypto: SaleCrypto.fromJson(asStringKeyedMap(json['crypto'])), - fiat: SaleFiat.fromJson(asStringKeyedMap(json['fiat'])), - fees: SaleFees.fromJson(asStringKeyedMap(json['fees'])), - exchangeRate: asString(json['exchangeRate']), - ); - } -} - -class SaleCrypto { - const SaleCrypto({this.amount, this.status, this.assetInfo}); - - final String? amount; - final String? status; - final AssetInfo? assetInfo; - - factory SaleCrypto.fromJson(Map? json) { - if (json == null) { - return const SaleCrypto(); - } - return SaleCrypto( - amount: asString(json['amount']), - status: json['status'] as String?, - assetInfo: AssetInfo.fromJson(asStringKeyedMap(json['assetInfo'])), - ); - } -} - -class SaleFiat { - const SaleFiat({this.amount, this.currencySymbol, this.status, this.payoutMethod}); - - final String? amount; - final String? currencySymbol; - final String? status; - final String? payoutMethod; - - factory SaleFiat.fromJson(Map? json) { - if (json == null) { - return const SaleFiat(); - } - return SaleFiat( - amount: asString(json['amount']), - currencySymbol: asString(json['currencySymbol']), - status: json['status'] as String?, - payoutMethod: json['payoutMethod'] as String?, - ); - } -} - -class SaleFees { - const SaleFees({this.amount, this.currencySymbol}); - - final String? amount; - final String? currencySymbol; - - factory SaleFees.fromJson(Map? json) { - if (json == null) { - return const SaleFees(); - } - return SaleFees(amount: asString(json['amount']), currencySymbol: asString(json['currencySymbol'])); - } -} diff --git a/lib/events/purchase_created.dart b/lib/events/purchase_created.dart index 6a1612a..24aae98 100644 --- a/lib/events/purchase_created.dart +++ b/lib/events/purchase_created.dart @@ -24,77 +24,3 @@ class PurchaseCreatedPayload { ); } } - -class PurchaseDetails { - const PurchaseDetails({ - this.id, - this.endTime, - this.asset, - this.receiverAddress, - this.cryptoAmount, - this.fiatCurrency, - this.fiatValue, - this.assetExchangeRate, - this.assetExchangeRateEur, - this.fiatExchangeRateEur, - this.baseRampFee, - this.networkFee, - this.appliedFee, - this.paymentMethodType, - this.finalTxHash, - this.createdAt, - this.updatedAt, - this.status, - this.purchaseViewToken, - this.hostFeeCut, - }); - - final String? id; - final String? endTime; - final AssetInfo? asset; - final String? receiverAddress; - final String? cryptoAmount; - final String? fiatCurrency; - final double? fiatValue; - final double? assetExchangeRate; - final double? assetExchangeRateEur; - final double? fiatExchangeRateEur; - final double? baseRampFee; - final double? networkFee; - final double? appliedFee; - final String? paymentMethodType; - final String? finalTxHash; - final String? createdAt; - final String? updatedAt; - final String? status; - final String? purchaseViewToken; - final double? hostFeeCut; - - factory PurchaseDetails.fromJson(Map? json) { - if (json == null) { - return const PurchaseDetails(); - } - return PurchaseDetails( - id: json['id'] as String?, - endTime: json['endTime'] as String?, - asset: AssetInfo.fromJson(asStringKeyedMap(json['asset'])), - receiverAddress: json['receiverAddress'] as String?, - cryptoAmount: asString(json['cryptoAmount']), - fiatCurrency: json['fiatCurrency'] as String?, - fiatValue: asDouble(json['fiatValue']), - assetExchangeRate: asDouble(json['assetExchangeRate']), - assetExchangeRateEur: asDouble(json['assetExchangeRateEur']), - fiatExchangeRateEur: asDouble(json['fiatExchangeRateEur']), - baseRampFee: asDouble(json['baseRampFee']), - networkFee: asDouble(json['networkFee']), - appliedFee: asDouble(json['appliedFee']), - paymentMethodType: json['paymentMethodType'] as String?, - finalTxHash: json['finalTxHash'] as String?, - createdAt: json['createdAt'] as String?, - updatedAt: json['updatedAt'] as String?, - status: json['status'] as String?, - purchaseViewToken: json['purchaseViewToken'] as String?, - hostFeeCut: asDouble(json['hostFeeCut']), - ); - } -} diff --git a/lib/events/request_crypto_account.dart b/lib/events/request_crypto_account.dart index 7dfbcc1..f5ad2bf 100644 --- a/lib/events/request_crypto_account.dart +++ b/lib/events/request_crypto_account.dart @@ -9,7 +9,6 @@ final class RequestCryptoAccount extends WidgetEvent { class RequestCryptoAccountPayload { const RequestCryptoAccountPayload({this.type, this.assetSymbol}); - /// Ramp's internal chain id (e.g. `"ETH"`, `"POLYGON"`). final String? type; final String? assetSymbol; diff --git a/lib/host_event.dart b/lib/host_event.dart index bc575d6..361f920 100644 --- a/lib/host_event.dart +++ b/lib/host_event.dart @@ -1,7 +1,6 @@ part 'host_events/request_crypto_account_result.dart'; part 'host_events/send_crypto_result.dart'; -/// Outgoing host → widget events for the Flutter SDK surface. sealed class HostEvent { const HostEvent(); diff --git a/lib/events/asset_info.dart b/lib/models/asset_info.dart similarity index 81% rename from lib/events/asset_info.dart rename to lib/models/asset_info.dart index 6f1f9a5..75b79af 100644 --- a/lib/events/asset_info.dart +++ b/lib/models/asset_info.dart @@ -1,6 +1,5 @@ -part of '../widget_event.dart'; +import 'package:ramp_flutter/models/json_map.dart'; -/// Asset metadata shared by purchase / send-crypto / sale payloads. class AssetInfo { const AssetInfo({ this.uai, @@ -12,7 +11,6 @@ class AssetInfo { this.decimals, }); - /// UAI is undefined for assets that don't have a SLIP-44 coin ID. final String? uai; final String? address; final String? symbol; diff --git a/lib/events/json_map.dart b/lib/models/json_map.dart similarity index 100% rename from lib/events/json_map.dart rename to lib/models/json_map.dart diff --git a/lib/models/purchase_details.dart b/lib/models/purchase_details.dart new file mode 100644 index 0000000..ae67877 --- /dev/null +++ b/lib/models/purchase_details.dart @@ -0,0 +1,76 @@ +import 'package:ramp_flutter/models/asset_info.dart'; +import 'package:ramp_flutter/models/json_map.dart'; + +class PurchaseDetails { + const PurchaseDetails({ + this.id, + this.endTime, + this.asset, + this.receiverAddress, + this.cryptoAmount, + this.fiatCurrency, + this.fiatValue, + this.assetExchangeRate, + this.assetExchangeRateEur, + this.fiatExchangeRateEur, + this.baseRampFee, + this.networkFee, + this.appliedFee, + this.paymentMethodType, + this.finalTxHash, + this.createdAt, + this.updatedAt, + this.status, + this.purchaseViewToken, + this.hostFeeCut, + }); + + final String? id; + final String? endTime; + final AssetInfo? asset; + final String? receiverAddress; + final String? cryptoAmount; + final String? fiatCurrency; + final double? fiatValue; + final double? assetExchangeRate; + final double? assetExchangeRateEur; + final double? fiatExchangeRateEur; + final double? baseRampFee; + final double? networkFee; + final double? appliedFee; + final String? paymentMethodType; + final String? finalTxHash; + final String? createdAt; + final String? updatedAt; + final String? status; + final String? purchaseViewToken; + final double? hostFeeCut; + + factory PurchaseDetails.fromJson(Map? json) { + if (json == null) { + return const PurchaseDetails(); + } + return PurchaseDetails( + id: json['id'] as String?, + endTime: json['endTime'] as String?, + asset: AssetInfo.fromJson(asStringKeyedMap(json['asset'])), + receiverAddress: json['receiverAddress'] as String?, + cryptoAmount: asString(json['cryptoAmount']), + fiatCurrency: json['fiatCurrency'] as String?, + fiatValue: asDouble(json['fiatValue']), + assetExchangeRate: asDouble(json['assetExchangeRate']), + assetExchangeRateEur: asDouble(json['assetExchangeRateEur']), + fiatExchangeRateEur: asDouble(json['fiatExchangeRateEur']), + baseRampFee: asDouble(json['baseRampFee']), + networkFee: asDouble(json['networkFee']), + appliedFee: asDouble(json['appliedFee']), + paymentMethodType: json['paymentMethodType'] as String?, + finalTxHash: json['finalTxHash'] as String?, + createdAt: json['createdAt'] as String?, + updatedAt: json['updatedAt'] as String?, + status: json['status'] as String?, + purchaseViewToken: json['purchaseViewToken'] as String?, + hostFeeCut: asDouble(json['hostFeeCut']), + ); + } +} diff --git a/lib/models/sale_details.dart b/lib/models/sale_details.dart new file mode 100644 index 0000000..fbb0dde --- /dev/null +++ b/lib/models/sale_details.dart @@ -0,0 +1,83 @@ +import 'package:ramp_flutter/models/asset_info.dart'; +import 'package:ramp_flutter/models/json_map.dart'; + +class SaleDetails { + const SaleDetails({this.id, this.createdAt, this.updatedAt, this.crypto, this.fiat, this.fees, this.exchangeRate}); + + final String? id; + final String? createdAt; + final String? updatedAt; + final SaleCrypto? crypto; + final SaleFiat? fiat; + final SaleFees? fees; + final String? exchangeRate; + + factory SaleDetails.fromJson(Map? json) { + if (json == null) { + return const SaleDetails(); + } + return SaleDetails( + id: json['id'] as String?, + createdAt: json['createdAt'] as String?, + updatedAt: json['updatedAt'] as String?, + crypto: SaleCrypto.fromJson(asStringKeyedMap(json['crypto'])), + fiat: SaleFiat.fromJson(asStringKeyedMap(json['fiat'])), + fees: SaleFees.fromJson(asStringKeyedMap(json['fees'])), + exchangeRate: asString(json['exchangeRate']), + ); + } +} + +class SaleCrypto { + const SaleCrypto({this.amount, this.status, this.assetInfo}); + + final String? amount; + final String? status; + final AssetInfo? assetInfo; + + factory SaleCrypto.fromJson(Map? json) { + if (json == null) { + return const SaleCrypto(); + } + return SaleCrypto( + amount: asString(json['amount']), + status: json['status'] as String?, + assetInfo: AssetInfo.fromJson(asStringKeyedMap(json['assetInfo'])), + ); + } +} + +class SaleFiat { + const SaleFiat({this.amount, this.currencySymbol, this.status, this.payoutMethod}); + + final String? amount; + final String? currencySymbol; + final String? status; + final String? payoutMethod; + + factory SaleFiat.fromJson(Map? json) { + if (json == null) { + return const SaleFiat(); + } + return SaleFiat( + amount: asString(json['amount']), + currencySymbol: asString(json['currencySymbol']), + status: json['status'] as String?, + payoutMethod: json['payoutMethod'] as String?, + ); + } +} + +class SaleFees { + const SaleFees({this.amount, this.currencySymbol}); + + final String? amount; + final String? currencySymbol; + + factory SaleFees.fromJson(Map? json) { + if (json == null) { + return const SaleFees(); + } + return SaleFees(amount: asString(json['amount']), currencySymbol: asString(json['currencySymbol'])); + } +} diff --git a/lib/ramp_flutter.dart b/lib/ramp_flutter.dart index 611b405..be5d625 100644 --- a/lib/ramp_flutter.dart +++ b/lib/ramp_flutter.dart @@ -4,7 +4,6 @@ import 'package:ramp_flutter/host_event.dart'; import 'package:ramp_flutter/src/ramp_webview.dart'; import 'package:ramp_flutter/widget_event.dart'; -/// Public API for embedding the Ramp Network widget. class RampFlutter { RampFlutter(Configuration configuration) : _webView = RampWebView(configuration.buildWidgetUrl()); @@ -22,10 +21,8 @@ class RampFlutter { Widget get view => _webView.view; - /// Posts a host → widget event (`HostEventTypes`). Future postHostEvent(HostEvent event) => _webView.postHostEvent(event); - /// Convenience for [SendCryptoResult.txHash]. Future sendCrypto(String? transactionHash) => _webView.sendCrypto(transactionHash); void dispose() => _webView.dispose(); diff --git a/lib/src/ramp_webview.dart b/lib/src/ramp_webview.dart index a4700d3..a04e2e9 100644 --- a/lib/src/ramp_webview.dart +++ b/lib/src/ramp_webview.dart @@ -8,7 +8,6 @@ import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter_android/webview_flutter_android.dart'; import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; -/// Owns the widget WebView. Not part of the public SDK surface. class RampWebView { RampWebView(this._widgetUrl); diff --git a/lib/widget_event.dart b/lib/widget_event.dart index e42c91f..77fbc13 100644 --- a/lib/widget_event.dart +++ b/lib/widget_event.dart @@ -1,6 +1,12 @@ -import 'package:ramp_flutter/events/json_map.dart'; +import 'package:ramp_flutter/models/asset_info.dart'; +import 'package:ramp_flutter/models/json_map.dart'; +import 'package:ramp_flutter/models/purchase_details.dart'; +import 'package:ramp_flutter/models/sale_details.dart'; + +export 'package:ramp_flutter/models/asset_info.dart'; +export 'package:ramp_flutter/models/purchase_details.dart'; +export 'package:ramp_flutter/models/sale_details.dart'; -part 'events/asset_info.dart'; part 'events/offramp_sale_created.dart'; part 'events/purchase_created.dart'; part 'events/request_crypto_account.dart'; @@ -9,7 +15,6 @@ part 'events/widget_close.dart'; part 'events/widget_config_done.dart'; part 'events/widget_config_failed.dart'; -/// Incoming widget → host events for the Flutter SDK surface. sealed class WidgetEvent { const WidgetEvent({this.widgetInstanceId}); From d816d0eb8be0a06f856029dcb9044f0467a6a237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 16:17:09 +0200 Subject: [PATCH 19/56] unify posting methods --- CHANGELOG.md | 7 +-- README.md | 5 +- example/lib/main.dart | 8 ++- lib/host_event.dart | 4 +- .../request_crypto_account_result.dart | 59 +++++++++++++++---- lib/host_events/send_crypto_result.dart | 36 +++++++++-- lib/ramp_flutter.dart | 2 - lib/src/ramp_webview.dart | 3 - test/ramp_webview_test.dart | 6 ++ 9 files changed, 99 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ca8141..4c7fcc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,9 @@ Android/iOS plugin shells). * Breaking: SDK no longer presents UI. Create `RampFlutter(configuration)`, embed `ramp.view` in your own route/sheet, and call `dispose` when done. -* Widget → host events: `WidgetConfigDone` / `Failed`, `PurchaseCreated`, - `OfframpSaleCreated`, `SendCryptoRequested`, `RequestCryptoAccount`, - `WidgetClose`. Host → widget: `SendCryptoResult` (`sendCrypto` convenience) - and `RequestCryptoAccountResult` via `postHostEvent`. +* Widget → host: sealed `WidgetEvent` via `onWidgetEvent`. Host → widget: sealed + `HostEvent` (`SendCryptoResult`, `RequestCryptoAccountResult`) via + `postHostEvent`. * Add `Configuration.offrampAsset` and build the widget URL in Dart (`Configuration.buildWidgetUrl`), including `sdkType` / `sdkVersion` / `variant`. diff --git a/README.md b/README.md index b0e03f3..360f6d4 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,10 @@ final ramp = RampFlutter(configuration) case OfframpSaleCreated(:final payload): break; case SendCryptoRequested(:final payload): - ramp.sendCrypto(txHash); - // or: ramp.postHostEvent(SendCryptoResult.txHash(txHash)); + ramp.postHostEvent(SendCryptoResult.txHash(txHash)); case RequestCryptoAccount(:final payload): ramp.postHostEvent( - RequestCryptoAccountResult.account(address: userAddress), + RequestCryptoAccountResult.account(address: userAddress, type: payload.type), ); case WidgetClose(): Navigator.of(context).pop(); diff --git a/example/lib/main.dart b/example/lib/main.dart index 7ef27ad..d5c4bd1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -97,14 +97,18 @@ class _RampFlutterAppState extends State { 'amount': payload.amount, 'asset': payload.assetInfo?.symbol, }); - ramp.sendCrypto('123'); + ramp.postHostEvent(SendCryptoResult.txHash('123')); case RequestCryptoAccount(:final payload): _addDebugEvent('REQUEST_CRYPTO_ACCOUNT', { 'type': payload.type, 'assetSymbol': payload.assetSymbol, }); ramp.postHostEvent( - RequestCryptoAccountResult.account(address: '0xabc', type: payload.type, assetSymbol: payload.assetSymbol), + RequestCryptoAccountResult.account( + address: '0xabc', + type: payload.type, + assetSymbol: payload.assetSymbol, + ), ); case WidgetClose(:final payload): _addDebugEvent('WIDGET_CLOSE', {'showAlert': payload.showAlert}); diff --git a/lib/host_event.dart b/lib/host_event.dart index 361f920..3576f70 100644 --- a/lib/host_event.dart +++ b/lib/host_event.dart @@ -6,7 +6,7 @@ sealed class HostEvent { String get type; - Object? get jsonPayload; + Map payloadToJson(); - Map toJson() => {'type': type, 'payload': ?jsonPayload}; + Map toJson() => {'type': type, 'payload': payloadToJson()}; } diff --git a/lib/host_events/request_crypto_account_result.dart b/lib/host_events/request_crypto_account_result.dart index 7543df5..5576024 100644 --- a/lib/host_events/request_crypto_account_result.dart +++ b/lib/host_events/request_crypto_account_result.dart @@ -1,28 +1,67 @@ part of '../host_event.dart'; final class RequestCryptoAccountResult extends HostEvent { - const RequestCryptoAccountResult._(this.payload); + const RequestCryptoAccountResult(this.payload); factory RequestCryptoAccountResult.account({ required String address, String? type, String? name, String? assetSymbol, - }) => RequestCryptoAccountResult._({ - 'address': address, - 'type': ?type, - 'name': ?name, - 'assetSymbol': ?assetSymbol, - }); + }) => RequestCryptoAccountResult( + RequestCryptoAccountSuccessPayload( + address: address, + type: type, + name: name, + assetSymbol: assetSymbol, + ), + ); factory RequestCryptoAccountResult.error([String? error]) => - RequestCryptoAccountResult._({'error': error}); + RequestCryptoAccountResult(RequestCryptoAccountErrorPayload(error)); - final Map payload; + final RequestCryptoAccountResultPayload payload; @override String get type => 'REQUEST_CRYPTO_ACCOUNT_RESULT'; @override - Object? get jsonPayload => payload; + Map payloadToJson() => payload.toJson(); +} + +sealed class RequestCryptoAccountResultPayload { + const RequestCryptoAccountResultPayload(); + + Map toJson(); +} + +final class RequestCryptoAccountSuccessPayload extends RequestCryptoAccountResultPayload { + const RequestCryptoAccountSuccessPayload({ + required this.address, + this.type, + this.name, + this.assetSymbol, + }); + + final String address; + final String? type; + final String? name; + final String? assetSymbol; + + @override + Map toJson() => { + 'address': address, + 'type': ?type, + 'name': ?name, + 'assetSymbol': ?assetSymbol, + }; +} + +final class RequestCryptoAccountErrorPayload extends RequestCryptoAccountResultPayload { + const RequestCryptoAccountErrorPayload([this.error]); + + final String? error; + + @override + Map toJson() => {'error': error}; } diff --git a/lib/host_events/send_crypto_result.dart b/lib/host_events/send_crypto_result.dart index 08be6e3..99f9f35 100644 --- a/lib/host_events/send_crypto_result.dart +++ b/lib/host_events/send_crypto_result.dart @@ -1,17 +1,43 @@ part of '../host_event.dart'; final class SendCryptoResult extends HostEvent { - const SendCryptoResult._(this.payload); + const SendCryptoResult(this.payload); - factory SendCryptoResult.txHash(String? txHash) => SendCryptoResult._({'txHash': txHash}); + factory SendCryptoResult.txHash(String? txHash) => + SendCryptoResult(SendCryptoResultTxHashPayload(txHash)); - factory SendCryptoResult.error([String? error]) => SendCryptoResult._({'error': error}); + factory SendCryptoResult.error([String? error]) => + SendCryptoResult(SendCryptoResultErrorPayload(error)); - final Map payload; + final SendCryptoResultPayload payload; @override String get type => 'SEND_CRYPTO_RESULT'; @override - Object? get jsonPayload => payload; + Map payloadToJson() => payload.toJson(); +} + +sealed class SendCryptoResultPayload { + const SendCryptoResultPayload(); + + Map toJson(); +} + +final class SendCryptoResultTxHashPayload extends SendCryptoResultPayload { + const SendCryptoResultTxHashPayload(this.txHash); + + final String? txHash; + + @override + Map toJson() => {'txHash': txHash}; +} + +final class SendCryptoResultErrorPayload extends SendCryptoResultPayload { + const SendCryptoResultErrorPayload([this.error]); + + final String? error; + + @override + Map toJson() => {'error': error}; } diff --git a/lib/ramp_flutter.dart b/lib/ramp_flutter.dart index be5d625..c52bd1d 100644 --- a/lib/ramp_flutter.dart +++ b/lib/ramp_flutter.dart @@ -23,8 +23,6 @@ class RampFlutter { Future postHostEvent(HostEvent event) => _webView.postHostEvent(event); - Future sendCrypto(String? transactionHash) => _webView.sendCrypto(transactionHash); - void dispose() => _webView.dispose(); @visibleForTesting diff --git a/lib/src/ramp_webview.dart b/lib/src/ramp_webview.dart index a04e2e9..a892577 100644 --- a/lib/src/ramp_webview.dart +++ b/lib/src/ramp_webview.dart @@ -123,9 +123,6 @@ class RampWebView { return webView.runJavaScript('window.postMessage($message, "${_widgetUrl.origin}");'); } - Future sendCrypto(String? transactionHash) => - postHostEvent(SendCryptoResult.txHash(transactionHash)); - void dispose() { _controller ?..removeJavaScriptChannel(_channelName) diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index 1cf6aa7..020e857 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -206,6 +206,9 @@ void main() { 'type': 'SEND_CRYPTO_RESULT', 'payload': {'error': 'failed'}, }); + expect(SendCryptoResult.txHash('0xhash').payload, isA()); + expect((SendCryptoResult.txHash('0xhash').payload as SendCryptoResultTxHashPayload).txHash, '0xhash'); + expect(RequestCryptoAccountResult.account(address: '0xabc', type: 'ETH').toJson(), { 'type': 'REQUEST_CRYPTO_ACCOUNT_RESULT', 'payload': {'address': '0xabc', 'type': 'ETH'}, @@ -214,6 +217,9 @@ void main() { 'type': 'REQUEST_CRYPTO_ACCOUNT_RESULT', 'payload': {'error': 'denied'}, }); + final account = RequestCryptoAccountResult.account(address: '0xabc', type: 'ETH'); + expect(account.payload, isA()); + expect((account.payload as RequestCryptoAccountSuccessPayload).address, '0xabc'); }); }); } From c0aa6ddc8783ec45c3d4e11bfbf803a978192e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 16:32:50 +0200 Subject: [PATCH 20/56] add widget close request event --- CHANGELOG.md | 6 +++--- README.md | 3 +++ example/lib/main.dart | 2 ++ lib/events/widget_close_request.dart | 5 +++++ lib/widget_event.dart | 3 +++ test/ramp_webview_test.dart | 2 ++ 6 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 lib/events/widget_close_request.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c7fcc9..e31fa15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,9 @@ Android/iOS plugin shells). * Breaking: SDK no longer presents UI. Create `RampFlutter(configuration)`, embed `ramp.view` in your own route/sheet, and call `dispose` when done. -* Widget → host: sealed `WidgetEvent` via `onWidgetEvent`. Host → widget: sealed - `HostEvent` (`SendCryptoResult`, `RequestCryptoAccountResult`) via - `postHostEvent`. +* Widget → host: sealed `WidgetEvent` via `onWidgetEvent` (includes + `WidgetCloseRequest`). Host → widget: sealed `HostEvent` + (`SendCryptoResult`, `RequestCryptoAccountResult`) via `postHostEvent`. * Add `Configuration.offrampAsset` and build the widget URL in Dart (`Configuration.buildWidgetUrl`), including `sdkType` / `sdkVersion` / `variant`. diff --git a/README.md b/README.md index 360f6d4..61592c8 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,9 @@ final ramp = RampFlutter(configuration) ); case WidgetClose(): Navigator.of(context).pop(); + case WidgetCloseRequest(): + // User tried to dismiss while widget is not closeable — confirm or ignore. + break; case WidgetConfigDone(): case WidgetConfigFailed(): break; diff --git a/example/lib/main.dart b/example/lib/main.dart index d5c4bd1..d0fe2fa 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -112,6 +112,8 @@ class _RampFlutterAppState extends State { ); case WidgetClose(:final payload): _addDebugEvent('WIDGET_CLOSE', {'showAlert': payload.showAlert}); + case WidgetCloseRequest(): + _addDebugEvent('WIDGET_CLOSE_REQUEST'); } }; diff --git a/lib/events/widget_close_request.dart b/lib/events/widget_close_request.dart new file mode 100644 index 0000000..670d523 --- /dev/null +++ b/lib/events/widget_close_request.dart @@ -0,0 +1,5 @@ +part of '../widget_event.dart'; + +final class WidgetCloseRequest extends WidgetEvent { + const WidgetCloseRequest({super.widgetInstanceId}); +} diff --git a/lib/widget_event.dart b/lib/widget_event.dart index 77fbc13..327e96b 100644 --- a/lib/widget_event.dart +++ b/lib/widget_event.dart @@ -12,6 +12,7 @@ part 'events/purchase_created.dart'; part 'events/request_crypto_account.dart'; part 'events/send_crypto_requested.dart'; part 'events/widget_close.dart'; +part 'events/widget_close_request.dart'; part 'events/widget_config_done.dart'; part 'events/widget_config_failed.dart'; @@ -70,6 +71,8 @@ sealed class WidgetEvent { payload: WidgetClosePayload.fromJson(payload), widgetInstanceId: widgetInstanceId, ); + case 'WIDGET_CLOSE_REQUEST': + return WidgetCloseRequest(widgetInstanceId: widgetInstanceId); default: return null; } diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index 020e857..e501813 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -141,6 +141,7 @@ void main() { }), ); ramp.handleJavaScriptMessage(jsonEncode({'type': 'CLOSE'})); + ramp.handleJavaScriptMessage(jsonEncode({'type': 'WIDGET_CLOSE_REQUEST', 'payload': null})); expect(events, [ isA(), @@ -151,6 +152,7 @@ void main() { isA(), isA(), isA(), + isA(), ]); expect(events[0].widgetInstanceId, 'w1'); From c5cf5e950a932932c3ddabb382735e61b2d5603e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 16:35:13 +0200 Subject: [PATCH 21/56] fix android file picker --- CHANGELOG.md | 2 + README.md | 5 +- example/pubspec.lock | 200 ++++++++++++++++++++++++++++++++++++++ lib/src/ramp_webview.dart | 73 ++++++++++++++ pubspec.lock | 200 ++++++++++++++++++++++++++++++++++++++ pubspec.yaml | 2 + 6 files changed, 481 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e31fa15..bae81de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ * Open `target=_blank` / `window.open` in the system browser via a forked `webview_flutter` platform package (`flutter_packages` / `webview-target-blank`). +* Android WebView file inputs use `file_picker`; capture requests use + `image_picker` (camera). ## 4.0.1 diff --git a/README.md b/README.md index 61592c8..4c5126f 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,9 @@ dependency_overrides: path: packages/webview_flutter/webview_flutter_platform_interface ``` -- Android document file upload from the WebView is not wired in this SDK yet. +- Android WebView `` uses `file_picker` (and the camera via + `image_picker` when capture is requested). Host apps need camera / photo + library usage descriptions (see Getting Started). iOS WKWebView handles file + inputs natively. - Server-signed widget URLs are not supported in this release; use `Configuration` fields so the SDK can build the widget URL. diff --git a/example/pubspec.lock b/example/pubspec.lock index 0cbc714..0997fc5 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -41,6 +49,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" + url: "https://pub.dev" + source: hosted + version: "0.3.5+4" + dbus: + dependency: transitive + description: + name: dbus + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + url: "https://pub.dev" + source: hosted + version: "0.7.14" fake_async: dependency: transitive description: @@ -49,6 +73,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file_picker: + dependency: transitive + description: + name: file_picker + sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" + url: "https://pub.dev" + source: hosted + version: "10.3.10" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" flutter: dependency: "direct main" description: flutter @@ -62,6 +134,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" + url: "https://pub.dev" + source: hosted + version: "2.0.35" flutter_test: dependency: "direct dev" description: flutter @@ -72,6 +152,86 @@ packages: description: flutter source: sdk version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image_picker: + dependency: transitive + description: + name: image_picker + sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a" + url: "https://pub.dev" + source: hosted + version: "0.8.13+19" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + url: "https://pub.dev" + source: hosted + version: "0.8.13+6" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" leak_tracker: dependency: transitive description: @@ -128,6 +288,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" path: dependency: transitive description: @@ -136,6 +304,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" plugin_platform_interface: dependency: transitive description: @@ -204,6 +380,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" url_launcher: dependency: transitive description: @@ -328,6 +512,22 @@ packages: url: "https://github.com/mateusz-ramp/flutter_packages.git" source: git version: "3.26.0" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" sdks: dart: ">=3.12.2 <4.0.0" flutter: ">=3.44.0" diff --git a/lib/src/ramp_webview.dart b/lib/src/ramp_webview.dart index a892577..218a16a 100644 --- a/lib/src/ramp_webview.dart +++ b/lib/src/ramp_webview.dart @@ -1,6 +1,8 @@ import 'dart:convert'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/widgets.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:ramp_flutter/host_event.dart'; import 'package:ramp_flutter/widget_event.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -83,12 +85,83 @@ class RampWebView { final platform = controller.platform; if (platform is AndroidWebViewController) { platform.setMediaPlaybackRequiresUserGesture(false); + platform.setOnShowFileSelector(_androidFileSelector); } controller.loadRequest(_widgetUrl); return controller; } + Future> _androidFileSelector(FileSelectorParams params) async { + try { + if (params.isCaptureEnabled) { + final photo = await ImagePicker().pickImage(source: ImageSource.camera); + if (photo == null) { + return const []; + } + return [Uri.file(photo.path).toString()]; + } + + final fileType = _fileTypeForAcceptTypes(params.acceptTypes); + final result = await FilePicker.platform.pickFiles( + allowMultiple: params.mode == FileSelectorMode.openMultiple, + type: fileType, + allowedExtensions: fileType == FileType.custom ? _extensionsForAcceptTypes(params.acceptTypes) : null, + ); + if (result == null) { + return const []; + } + return [ + for (final file in result.files) + if (file.path != null) Uri.file(file.path!).toString(), + ]; + } catch (error, stackTrace) { + debugPrint('RampFlutter: file selection failed: $error\n$stackTrace'); + return const []; + } + } + + static FileType _fileTypeForAcceptTypes(List acceptTypes) { + if (acceptTypes.isEmpty) { + return FileType.any; + } + final normalized = acceptTypes.map((type) => type.toLowerCase().trim()).toList(); + final onlyImages = normalized.every((type) => type.startsWith('image/')); + if (onlyImages) { + return FileType.image; + } + final onlyVideos = normalized.every((type) => type.startsWith('video/')); + if (onlyVideos) { + return FileType.video; + } + final extensions = _extensionsForAcceptTypes(acceptTypes); + final allMappedToExtensions = normalized.every( + (type) => type.startsWith('.') || type == 'application/pdf', + ); + if (allMappedToExtensions && extensions != null && extensions.isNotEmpty) { + return FileType.custom; + } + return FileType.any; + } + + static List? _extensionsForAcceptTypes(List acceptTypes) { + final extensions = {}; + for (final type in acceptTypes) { + final value = type.trim().toLowerCase(); + if (value.startsWith('.')) { + extensions.add(value.substring(1)); + continue; + } + if (value == 'application/pdf') { + extensions.add('pdf'); + } + } + if (extensions.isEmpty) { + return null; + } + return extensions.toList(); + } + Future _openExternal(Uri uri) async { try { if (uri.scheme == 'intent') { diff --git a/pubspec.lock b/pubspec.lock index 9b7da79..a46c73b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: @@ -41,6 +49,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" + url: "https://pub.dev" + source: hosted + version: "0.3.5+4" + dbus: + dependency: transitive + description: + name: dbus + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + url: "https://pub.dev" + source: hosted + version: "0.7.14" fake_async: dependency: transitive description: @@ -49,6 +73,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" + url: "https://pub.dev" + source: hosted + version: "10.3.10" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://pub.dev" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://pub.dev" + source: hosted + version: "0.9.3+5" flutter: dependency: "direct main" description: flutter @@ -62,6 +134,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" + url: "https://pub.dev" + source: hosted + version: "2.0.35" flutter_test: dependency: "direct dev" description: flutter @@ -72,6 +152,86 @@ packages: description: flutter source: sdk version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a" + url: "https://pub.dev" + source: hosted + version: "0.8.13+19" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + url: "https://pub.dev" + source: hosted + version: "0.8.13+6" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://pub.dev" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://pub.dev" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://pub.dev" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://pub.dev" + source: hosted + version: "0.2.2" leak_tracker: dependency: transitive description: @@ -128,6 +288,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" path: dependency: transitive description: @@ -136,6 +304,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" plugin_platform_interface: dependency: transitive description: @@ -197,6 +373,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" url_launcher: dependency: "direct main" description: @@ -321,6 +505,22 @@ packages: url: "https://github.com/mateusz-ramp/flutter_packages.git" source: git version: "3.26.0" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" sdks: dart: ">=3.12.2 <4.0.0" flutter: ">=3.44.0" diff --git a/pubspec.yaml b/pubspec.yaml index 299c11f..cd311a3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,8 @@ environment: dependencies: flutter: sdk: flutter + file_picker: ^10.1.2 + image_picker: ^1.1.2 url_launcher: ^6.3.2 webview_flutter: ^4.14.1 webview_flutter_android: ^4.13.0 From fcd4ec7c6c2d745ffeb52751a1be7fd4c11e6588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 16:37:44 +0200 Subject: [PATCH 22/56] open non-http/s urls externally --- CHANGELOG.md | 6 +++--- README.md | 10 +++++----- lib/src/ramp_webview.dart | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bae81de..1085b6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,9 +17,9 @@ * Raise minimum platforms to Android 7.0 (API 24) and iOS 13. * Raise minimum Flutter to 3.44 / Dart 3.12; update `webview_flutter`, `url_launcher`, and `flutter_lints`. -* Open `target=_blank` / `window.open` in the system browser via a forked - `webview_flutter` platform package (`flutter_packages` / - `webview-target-blank`). +* Open off-widget navigations and `target=_blank` / `window.open` in the system + browser (same-host stays in the WebView). `window.open` needs a forked + `webview_flutter` (`flutter_packages` / `webview-target-blank`). * Android WebView file inputs use `file_picker`; capture requests use `image_picker` (camera). diff --git a/README.md b/README.md index 4c5126f..b771ef3 100644 --- a/README.md +++ b/README.md @@ -79,12 +79,12 @@ For more configuration parameters see ### Notes -- `target=_blank` / `window.open` use `NavigationDelegate.onCreateWindow` - via a fork of `webview_flutter` +- Off-widget navigations (other https hosts, custom schemes, `intent:`) and + `target=_blank` / `window.open` open in the system browser via + `NavigationDelegate` + a fork of `webview_flutter` ([mateusz-ramp/flutter_packages](https://github.com/mateusz-ramp/flutter_packages/tree/webview-target-blank) - branch `webview-target-blank`), matching native `createWebView` / - `onCreateWindow`. The SDK opens those URLs in the system browser. Host apps - (and `example/`) must declare matching `dependency_overrides`: + branch `webview-target-blank`). Host apps (and `example/`) must declare + matching `dependency_overrides`: ```yaml dependency_overrides: diff --git a/lib/src/ramp_webview.dart b/lib/src/ramp_webview.dart index 218a16a..04ca878 100644 --- a/lib/src/ramp_webview.dart +++ b/lib/src/ramp_webview.dart @@ -50,6 +50,15 @@ class RampWebView { ..setNavigationDelegate( NavigationDelegate( onNavigationRequest: (request) { + final uri = Uri.tryParse(request.url); + if (uri == null) { + return NavigationDecision.prevent; + } + if (_shouldOpenExternally(uri)) { + debugPrint('RampFlutter: open external (navigation) ${request.url}'); + _openExternal(uri); + return NavigationDecision.prevent; + } debugPrint('RampFlutter: allow navigation ${request.url}'); return NavigationDecision.navigate; }, @@ -162,6 +171,15 @@ class RampWebView { return extensions.toList(); } + bool _shouldOpenExternally(Uri uri) { + final scheme = uri.scheme.toLowerCase(); + final isHttp = scheme == 'http' || scheme == 'https'; + if (!isHttp) { + return true; + } + return uri.host.toLowerCase() != _widgetUrl.host.toLowerCase(); + } + Future _openExternal(Uri uri) async { try { if (uri.scheme == 'intent') { From e4e96340081e3643ffa8c7bad35c2a8350e9ceda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 16:39:11 +0200 Subject: [PATCH 23/56] ensure controller --- lib/src/ramp_webview.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/src/ramp_webview.dart b/lib/src/ramp_webview.dart index 04ca878..a133ee4 100644 --- a/lib/src/ramp_webview.dart +++ b/lib/src/ramp_webview.dart @@ -206,10 +206,7 @@ class RampWebView { } Future postHostEvent(HostEvent event) { - final webView = _controller; - if (webView == null) { - return Future.value(); - } + final webView = _ensureController(); final message = jsonEncode(event.toJson()); return webView.runJavaScript('window.postMessage($message, "${_widgetUrl.origin}");'); } From 049c02f5f3ea34245fa15cd2b6b0e28eb6dba17a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 16:40:41 +0200 Subject: [PATCH 24/56] make Configuration an url builder --- CHANGELOG.md | 11 ++-- README.md | 18 +++--- example/lib/main.dart | 108 ++++++++++++++++++++---------------- lib/configuration.dart | 72 +++++++++++++++--------- lib/ramp_flutter.dart | 7 +-- test/ramp_webview_test.dart | 36 +++++------- 6 files changed, 142 insertions(+), 110 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1085b6a..67b583d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,15 @@ * Rewrite the SDK to load the Ramp widget in a Flutter WebView instead of the native iOS/Android Ramp SDKs. Published as a Dart Flutter package (no Android/iOS plugin shells). -* Breaking: SDK no longer presents UI. Create `RampFlutter(configuration)`, - embed `ramp.view` in your own route/sheet, and call `dispose` when done. +* Breaking: SDK no longer presents UI. Create `RampFlutter(widgetUrl)` (or + `RampFlutter.fromConfiguration`), embed `ramp.view` in your own route/sheet, + and call `dispose` when done. `Configuration` is an immutable helper that + only builds the widget URL. * Widget → host: sealed `WidgetEvent` via `onWidgetEvent` (includes `WidgetCloseRequest`). Host → widget: sealed `HostEvent` (`SendCryptoResult`, `RequestCryptoAccountResult`) via `postHostEvent`. -* Add `Configuration.offrampAsset` and build the widget URL in Dart - (`Configuration.buildWidgetUrl`), including `sdkType` / `sdkVersion` / - `variant`. +* Add `Configuration.offrampAsset` and `Configuration.buildWidgetUrl`, including + `sdkType` / `sdkVersion` / `variant`. * Default base URL is now `https://app.rampnetwork.com`. * Raise minimum platforms to Android 7.0 (API 24) and iOS 13. * Raise minimum Flutter to 3.44 / Dart 3.12; update `webview_flutter`, diff --git a/README.md b/README.md index b771ef3..a06dc5e 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,14 @@ The SDK provides the Ramp WebView; your app owns presentation (route, bottom sheet, dialog, etc.) and must call `dispose` when it is dismissed. ```dart -final ramp = RampFlutter(configuration) +final widgetUrl = Configuration( + hostApiKey: 'YOUR_API_KEY', + hostAppName: 'My App', + hostLogoUrl: 'https://example.com/logo.png', + enabledFlows: ['ONRAMP', 'OFFRAMP'], +).buildWidgetUrl(); + +final ramp = RampFlutter(widgetUrl) ..onWidgetEvent = (event) { switch (event) { case PurchaseCreated(:final payload): @@ -66,13 +73,8 @@ await showModalBottomSheet( ramp.dispose(); ``` -```dart -final configuration = Configuration() - ..hostApiKey = 'YOUR_API_KEY' - ..hostAppName = 'My App' - ..hostLogoUrl = 'https://example.com/logo.png' - ..enabledFlows = ['ONRAMP', 'OFFRAMP']; -``` +`Configuration` only builds the widget [Uri]; `RampFlutter` takes that URL (or use +`RampFlutter.fromConfiguration(...)`). For more configuration parameters see [Ramp Network Flutter documentation](https://docs.ramp.network/mobile/flutter-sdk/). diff --git a/example/lib/main.dart b/example/lib/main.dart index d0fe2fa..c301964 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -22,29 +22,30 @@ class RampFlutterApp extends StatefulWidget { } class _RampFlutterAppState extends State { - final Configuration _configuration = Configuration(); final ValueNotifier> _debugEvents = ValueNotifier>(const []); var _nextDebugEventId = 0; final List _predefinedEnvironments = [ - "https://app.dev.ramp-network.org", - "https://app.demo.ramp.network", - "https://app.rampnetwork.com", + 'https://app.dev.ramp-network.org', + 'https://app.demo.ramp.network', + 'https://app.rampnetwork.com', ]; int _selectedEnvironment = 0; + String? _userEmailAddress; + String? _fiatValue; + String? _fiatCurrency; + String? _defaultAsset = 'BTC_BTC'; + String? _offrampAsset; + String? _userAddress; + String? _hostAppName = 'Ramp Network Flutter'; + String? _hostApiKey; + String? _defaultFlow = 'ONRAMP'; + List _enabledFlows = ['ONRAMP', 'OFFRAMP', 'SWAP']; @override void initState() { - _configuration.hostAppName = "Ramp Network Flutter"; - _configuration.hostLogoUrl = "https://assets.rampnetwork.com/misc/ramp-network-logo.svg"; - _configuration.defaultFlow = "ONRAMP"; - _configuration.enabledFlows = ["ONRAMP", "OFFRAMP", "SWAP"]; - _configuration.defaultAsset = "BTC_BTC"; - _configuration.useSendCryptoCallback = true; - _configuration.deepLinkScheme = "rampflutterdemo"; _applyEnvironment(_selectedEnvironment); - super.initState(); } @@ -61,8 +62,26 @@ class _RampFlutterAppState extends State { void _applyEnvironment(int id) { _selectedEnvironment = id; - _configuration.url = _predefinedEnvironments[id]; - _configuration.hostApiKey = id == 0 ? ExampleSecrets.hostApiKeyInternal : null; + _hostApiKey = id == 0 ? ExampleSecrets.hostApiKeyInternal : null; + } + + Configuration _buildConfiguration() { + return Configuration( + url: _predefinedEnvironments[_selectedEnvironment], + hostAppName: _hostAppName, + hostLogoUrl: 'https://assets.rampnetwork.com/misc/ramp-network-logo.svg', + defaultFlow: _defaultFlow, + enabledFlows: List.from(_enabledFlows), + defaultAsset: _defaultAsset, + offrampAsset: _offrampAsset, + useSendCryptoCallback: true, + deepLinkScheme: 'rampflutterdemo', + hostApiKey: _hostApiKey, + userEmailAddress: _userEmailAddress, + fiatValue: _fiatValue, + fiatCurrency: _fiatCurrency, + userAddress: _userAddress, + ); } void _addDebugEvent(String label, [Map data = const {}]) { @@ -76,7 +95,7 @@ class _RampFlutterAppState extends State { } Future _showRamp(BuildContext context) async { - final ramp = RampFlutter(_configuration); + final ramp = RampFlutter.fromConfiguration(_buildConfiguration()); ramp.onWidgetEvent = (event) { switch (event) { @@ -202,58 +221,53 @@ class _RampFlutterAppState extends State { } Widget _appInfo() { - return const Text("App version: Flutter WebView"); + return const Text('App version: Flutter WebView'); } List _configurationForm() { return [ - _segmentedControl("Env:", ["dev", "demo", "prod"], _selectEnvironment), + _segmentedControl('Env:', ['dev', 'demo', 'prod'], _selectEnvironment), Text( _predefinedEnvironments[_selectedEnvironment], style: const TextStyle(color: Color.fromRGBO(46, 190, 117, 1)), ), - _textField( - "User email address", - (text) => _configuration.userEmailAddress = text, - _configuration.userEmailAddress, - ), - _textField("Fiat value", (text) => _configuration.fiatValue = text, _configuration.fiatValue), - _textField("Fiat currency", (text) => _configuration.fiatCurrency = text, _configuration.fiatCurrency), - _textField("Default asset", (text) => _configuration.defaultAsset = text, _configuration.defaultAsset), - _textField("Offramp asset", (text) => _configuration.offrampAsset = text, _configuration.offrampAsset), - _textField("User address", (text) => _configuration.userAddress = text, _configuration.userAddress), - _textField("Host app name", (text) => _configuration.hostAppName = text, _configuration.hostAppName), - _textField("Host API key", (text) => _configuration.hostApiKey = text, _configuration.hostApiKey), - _segmentedControl("Default flow:", ["ONRAMP", "OFFRAMP"], (index) { + _textField('User email address', (text) => _userEmailAddress = text, _userEmailAddress), + _textField('Fiat value', (text) => _fiatValue = text, _fiatValue), + _textField('Fiat currency', (text) => _fiatCurrency = text, _fiatCurrency), + _textField('Default asset', (text) => _defaultAsset = text, _defaultAsset), + _textField('Offramp asset', (text) => _offrampAsset = text, _offrampAsset), + _textField('User address', (text) => _userAddress = text, _userAddress), + _textField('Host app name', (text) => _hostAppName = text, _hostAppName), + _textField('Host API key', (text) => _hostApiKey = text, _hostApiKey), + _segmentedControl('Default flow:', ['ONRAMP', 'OFFRAMP'], (index) { if (index == 0) { - _configuration.defaultFlow = "ONRAMP"; + _defaultFlow = 'ONRAMP'; } if (index == 1) { - _configuration.defaultFlow = "OFFRAMP"; + _defaultFlow = 'OFFRAMP'; } + setState(() {}); }), - _enabledFlows(), + _enabledFlowsSection(), ]; } - Widget _enabledFlows() { - final flows = _configuration.enabledFlows ?? []; - + Widget _enabledFlowsSection() { Widget flowSwitch(String name) { return Row( mainAxisSize: MainAxisSize.min, children: [ Text(name), Switch( - value: flows.contains(name), + value: _enabledFlows.contains(name), onChanged: (enabled) { - if (enabled) { - flows.add(name); - } else { - flows.remove(name); - } - _configuration.enabledFlows = flows; - setState(() {}); + setState(() { + if (enabled) { + _enabledFlows = [..._enabledFlows, name]; + } else { + _enabledFlows = _enabledFlows.where((flow) => flow != name).toList(); + } + }); }, ), ], @@ -263,14 +277,14 @@ class _RampFlutterAppState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text("Enabled flows:"), - Row(children: [flowSwitch("ONRAMP"), flowSwitch("OFFRAMP"), flowSwitch("SWAP")]), + const Text('Enabled flows:'), + Row(children: [flowSwitch('ONRAMP'), flowSwitch('OFFRAMP'), flowSwitch('SWAP')]), ], ); } Widget _showRampButton(BuildContext context) { - return TextButton(onPressed: () => _showRamp(context), child: const Text("Show Ramp")); + return TextButton(onPressed: () => _showRamp(context), child: const Text('Show Ramp')); } Row _segmentedControl(String title, List options, void Function(int) itemSelected) { diff --git a/lib/configuration.dart b/lib/configuration.dart index 0685fe8..d43d723 100644 --- a/lib/configuration.dart +++ b/lib/configuration.dart @@ -1,34 +1,56 @@ +/// Builds a Ramp widget [Uri]. Not held by [RampFlutter] after construction — +/// call [buildWidgetUrl] and pass the result into [RampFlutter]. class Configuration { + const Configuration({ + this.url, + this.containerNode, + this.deepLinkScheme, + this.defaultAsset, + this.defaultFlow, + this.enabledFlows, + this.fiatCurrency, + this.fiatValue, + this.finalUrl, + this.hostApiKey, + this.hostAppName, + this.hostLogoUrl, + this.offrampAsset, + this.offrampWebhookV3Url, + this.selectedCountryCode, + this.swapAmount, + this.swapAsset, + this.userAddress, + this.userEmailAddress, + this.useSendCryptoCallback, + this.webhookStatusUrl, + }); + static const String defaultUrl = 'https://app.rampnetwork.com'; static const String sdkType = 'FLUTTER'; static const String sdkVersion = '5.0.0'; static const String mobileSdkVariant = 'sdk-mobile'; - String? url; - - String? containerNode; - String? deepLinkScheme; - String? defaultAsset; - String? defaultFlow; - List? enabledFlows; - String? fiatCurrency; - String? fiatValue; - String? finalUrl; - String? hostApiKey; - String? hostAppName; - String? hostLogoUrl; - String? offrampAsset; - String? offrampWebhookV3Url; - String? selectedCountryCode; - String? swapAmount; - String? swapAsset; - String? userAddress; - String? userEmailAddress; - bool? useSendCryptoCallback; - - /// Ignored when building the URL; the SDK always sends [mobileSdkVariant]. - String? variant; - String? webhookStatusUrl; + final String? url; + final String? containerNode; + final String? deepLinkScheme; + final String? defaultAsset; + final String? defaultFlow; + final List? enabledFlows; + final String? fiatCurrency; + final String? fiatValue; + final String? finalUrl; + final String? hostApiKey; + final String? hostAppName; + final String? hostLogoUrl; + final String? offrampAsset; + final String? offrampWebhookV3Url; + final String? selectedCountryCode; + final String? swapAmount; + final String? swapAsset; + final String? userAddress; + final String? userEmailAddress; + final bool? useSendCryptoCallback; + final String? webhookStatusUrl; Uri buildWidgetUrl() { final base = Uri.parse((url != null && url!.trim().isNotEmpty) ? url!.trim() : defaultUrl); diff --git a/lib/ramp_flutter.dart b/lib/ramp_flutter.dart index c52bd1d..9ee0de4 100644 --- a/lib/ramp_flutter.dart +++ b/lib/ramp_flutter.dart @@ -5,11 +5,10 @@ import 'package:ramp_flutter/src/ramp_webview.dart'; import 'package:ramp_flutter/widget_event.dart'; class RampFlutter { - RampFlutter(Configuration configuration) : _webView = RampWebView(configuration.buildWidgetUrl()); + RampFlutter(Uri widgetUrl) : _webView = RampWebView(widgetUrl); - /// Test-only: load a concrete widget URL without going through [Configuration]. - @visibleForTesting - RampFlutter.withWidgetUrl(Uri widgetUrl) : _webView = RampWebView(widgetUrl); + factory RampFlutter.fromConfiguration(Configuration configuration) => + RampFlutter(configuration.buildWidgetUrl()); final RampWebView _webView; diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index e501813..8bf8ac4 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -9,7 +9,7 @@ import 'package:ramp_flutter/widget_event.dart'; void main() { group('Configuration.buildWidgetUrl', () { test('uses default base URL and SDK metadata', () { - final url = Configuration().buildWidgetUrl(); + final url = const Configuration().buildWidgetUrl(); expect(url.scheme, 'https'); expect(url.host, 'app.rampnetwork.com'); @@ -19,18 +19,16 @@ void main() { }); test('merges configuration fields and joins enabled flows', () { - final url = - (Configuration() - ..url = 'https://app.dev.ramp-network.org/custom' - ..hostApiKey = 'key' - ..hostAppName = 'App' - ..offrampAsset = 'ETH' - ..offrampWebhookV3Url = 'https://example.com/hook' - ..enabledFlows = ['ONRAMP', 'OFFRAMP'] - ..defaultFlow = 'OFFRAMP' - ..useSendCryptoCallback = true - ..variant = 'ignored') - .buildWidgetUrl(); + final url = const Configuration( + url: 'https://app.dev.ramp-network.org/custom', + hostApiKey: 'key', + hostAppName: 'App', + offrampAsset: 'ETH', + offrampWebhookV3Url: 'https://example.com/hook', + enabledFlows: ['ONRAMP', 'OFFRAMP'], + defaultFlow: 'OFFRAMP', + useSendCryptoCallback: true, + ).buildWidgetUrl(); expect(url.host, 'app.dev.ramp-network.org'); expect(url.path, '/custom'); @@ -41,11 +39,7 @@ void main() { }); test('omits null and empty optional fields', () { - final url = - (Configuration() - ..hostApiKey = '' - ..fiatValue = null) - .buildWidgetUrl(); + final url = const Configuration(hostApiKey: '', fiatValue: null).buildWidgetUrl(); expect(url.queryParameters.containsKey('hostApiKey'), isFalse); expect(url.queryParameters.containsKey('fiatValue'), isFalse); @@ -57,7 +51,7 @@ void main() { test('ignores non-JSON and unknown types', () { final events = []; - final ramp = RampFlutter.withWidgetUrl(widgetUrl)..onWidgetEvent = events.add; + final ramp = RampFlutter(widgetUrl)..onWidgetEvent = events.add; ramp.handleJavaScriptMessage('not json'); ramp.handleJavaScriptMessage('42'); @@ -68,7 +62,7 @@ void main() { test('parses supported widget events', () { final events = []; - final ramp = RampFlutter.withWidgetUrl(widgetUrl)..onWidgetEvent = events.add; + final ramp = RampFlutter(widgetUrl)..onWidgetEvent = events.add; ramp.handleJavaScriptMessage( jsonEncode({'type': 'WIDGET_CONFIG_DONE', 'payload': null, 'widgetInstanceId': 'w1'}), @@ -184,7 +178,7 @@ void main() { test('rejects unsupported SEND_CRYPTO eventVersion', () { final events = []; - final ramp = RampFlutter.withWidgetUrl(widgetUrl)..onWidgetEvent = events.add; + final ramp = RampFlutter(widgetUrl)..onWidgetEvent = events.add; ramp.handleJavaScriptMessage( jsonEncode({ From 73c816e96b71c8a2370f85a86e58a1da588831c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 16:42:52 +0200 Subject: [PATCH 25/56] add debug prints --- lib/src/ramp_webview.dart | 17 ++++++++++++++--- lib/widget_event.dart | 9 ++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/lib/src/ramp_webview.dart b/lib/src/ramp_webview.dart index a133ee4..8eeabac 100644 --- a/lib/src/ramp_webview.dart +++ b/lib/src/ramp_webview.dart @@ -42,6 +42,7 @@ class RampWebView { if (onlyCamera) { request.grant(); } else { + debugPrint('RampFlutter: deny WebView permission types=${request.types}'); request.deny(); } }, @@ -52,6 +53,7 @@ class RampWebView { onNavigationRequest: (request) { final uri = Uri.tryParse(request.url); if (uri == null) { + debugPrint('RampFlutter: block navigation — invalid URL ${request.url}'); return NavigationDecision.prevent; } if (_shouldOpenExternally(uri)) { @@ -65,9 +67,11 @@ class RampWebView { onCreateWindow: (url) { debugPrint('RampFlutter: open external (create window) $url'); final uri = Uri.tryParse(url); - if (uri != null) { - _openExternal(uri); + if (uri == null) { + debugPrint('RampFlutter: create window ignored — invalid URL $url'); + return; } + _openExternal(uri); }, onPageStarted: (url) => debugPrint('RampFlutter: page started $url'), onPageFinished: (url) => debugPrint('RampFlutter: page finished $url'), @@ -186,11 +190,15 @@ class RampWebView { final fallback = _intentFallbackUrl(uri); if (fallback != null) { await launchUrl(fallback, mode: LaunchMode.externalApplication); + } else { + debugPrint('RampFlutter: intent URL has no browser_fallback_url: $uri'); } return; } if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + debugPrint('RampFlutter: cannot launch URL $uri'); } } catch (error, stackTrace) { debugPrint('RampFlutter: failed to open $uri: $error\n$stackTrace'); @@ -208,6 +216,7 @@ class RampWebView { Future postHostEvent(HostEvent event) { final webView = _ensureController(); final message = jsonEncode(event.toJson()); + debugPrint('RampFlutter: postHostEvent ${event.type}'); return webView.runJavaScript('window.postMessage($message, "${_widgetUrl.origin}");'); } @@ -222,10 +231,12 @@ class RampWebView { final dynamic decoded; try { decoded = jsonDecode(message); - } on FormatException { + } on FormatException catch (error) { + debugPrint('RampFlutter: drop JS message — invalid JSON: $error'); return; } if (decoded is! Map) { + debugPrint('RampFlutter: drop JS message — expected Map, got ${decoded.runtimeType}'); return; } final event = WidgetEvent.tryParse(Map.from(decoded)); diff --git a/lib/widget_event.dart b/lib/widget_event.dart index 327e96b..e7b0078 100644 --- a/lib/widget_event.dart +++ b/lib/widget_event.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:ramp_flutter/models/asset_info.dart'; import 'package:ramp_flutter/models/json_map.dart'; import 'package:ramp_flutter/models/purchase_details.dart'; @@ -24,6 +25,7 @@ sealed class WidgetEvent { static WidgetEvent? tryParse(Map json) { final type = json['type']; if (type is! String) { + debugPrint('RampFlutter: drop widget event — missing or non-string type ($type)'); return null; } final payload = asStringKeyedMap(json['payload']); @@ -48,9 +50,11 @@ sealed class WidgetEvent { case 'SEND_CRYPTO': final version = json['eventVersion']; if (version != null && version != 1) { + debugPrint('RampFlutter: drop SEND_CRYPTO — unsupported eventVersion=$version'); return null; } if (payload == null) { + debugPrint('RampFlutter: drop SEND_CRYPTO — missing payload'); return null; } return SendCryptoRequested( @@ -59,6 +63,7 @@ sealed class WidgetEvent { ); case 'REQUEST_CRYPTO_ACCOUNT': if (payload == null) { + debugPrint('RampFlutter: drop REQUEST_CRYPTO_ACCOUNT — missing payload'); return null; } return RequestCryptoAccount( @@ -74,9 +79,11 @@ sealed class WidgetEvent { case 'WIDGET_CLOSE_REQUEST': return WidgetCloseRequest(widgetInstanceId: widgetInstanceId); default: + debugPrint('RampFlutter: drop widget event — unknown type=$type'); return null; } - } catch (_) { + } catch (error, stackTrace) { + debugPrint('RampFlutter: drop widget event type=$type — parse error: $error\n$stackTrace'); return null; } } From 1aed4447c6d1f2df0e7d57ac3b93b63075696825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 16:47:59 +0200 Subject: [PATCH 26/56] fix webview packaging --- CHANGELOG.md | 5 +++-- README.md | 32 ++++---------------------------- example/pubspec.lock | 16 ++++++++-------- example/pubspec.yaml | 24 ------------------------ pubspec.lock | 10 +++++----- pubspec.yaml | 32 +++++++++++--------------------- 6 files changed, 31 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b583d..d4f265e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,9 @@ * Raise minimum Flutter to 3.44 / Dart 3.12; update `webview_flutter`, `url_launcher`, and `flutter_lints`. * Open off-widget navigations and `target=_blank` / `window.open` in the system - browser (same-host stays in the WebView). `window.open` needs a forked - `webview_flutter` (`flutter_packages` / `webview-target-blank`). + browser (same-host stays in the WebView). Depends on a forked + `webview_flutter` (`flutter_packages` / `webview-target-blank`) as a direct + git dependency so hosts pick it up transitively. * Android WebView file inputs use `file_picker`; capture requests use `image_picker` (camera). diff --git a/README.md b/README.md index a06dc5e..ea37852 100644 --- a/README.md +++ b/README.md @@ -83,35 +83,11 @@ For more configuration parameters see - Off-widget navigations (other https hosts, custom schemes, `intent:`) and `target=_blank` / `window.open` open in the system browser via - `NavigationDelegate` + a fork of `webview_flutter` + `NavigationDelegate` and a forked `webview_flutter` ([mateusz-ramp/flutter_packages](https://github.com/mateusz-ramp/flutter_packages/tree/webview-target-blank) - branch `webview-target-blank`). Host apps (and `example/`) must declare - matching `dependency_overrides`: - -```yaml -dependency_overrides: - webview_flutter: - git: - url: https://github.com/mateusz-ramp/flutter_packages.git - ref: webview-target-blank - path: packages/webview_flutter/webview_flutter - webview_flutter_android: - git: - url: https://github.com/mateusz-ramp/flutter_packages.git - ref: webview-target-blank - path: packages/webview_flutter/webview_flutter_android - webview_flutter_wkwebview: - git: - url: https://github.com/mateusz-ramp/flutter_packages.git - ref: webview-target-blank - path: packages/webview_flutter/webview_flutter_wkwebview - webview_flutter_platform_interface: - git: - url: https://github.com/mateusz-ramp/flutter_packages.git - ref: webview-target-blank - path: packages/webview_flutter/webview_flutter_platform_interface -``` - + / commit `a37d157`), declared as a direct dependency of this package so hosts + pick it up transitively (no host `dependency_overrides` required unless + another package pins pub.dev `webview_flutter`). - Android WebView `` uses `file_picker` (and the camera via `image_picker` when capture is requested). Host apps need camera / photo library usage descriptions (see Getting Started). iOS WKWebView handles file diff --git a/example/pubspec.lock b/example/pubspec.lock index 0997fc5..ab8e65f 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -477,37 +477,37 @@ packages: source: hosted version: "1.1.1" webview_flutter: - dependency: "direct overridden" + dependency: transitive description: path: "packages/webview_flutter/webview_flutter" - ref: webview-target-blank + ref: a37d157a9160fbd509484e9bb176be038c4d153c resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c url: "https://github.com/mateusz-ramp/flutter_packages.git" source: git version: "4.14.1" webview_flutter_android: - dependency: "direct overridden" + dependency: transitive description: path: "packages/webview_flutter/webview_flutter_android" - ref: webview-target-blank + ref: a37d157a9160fbd509484e9bb176be038c4d153c resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c url: "https://github.com/mateusz-ramp/flutter_packages.git" source: git version: "4.13.0" webview_flutter_platform_interface: - dependency: "direct overridden" + dependency: transitive description: path: "packages/webview_flutter/webview_flutter_platform_interface" - ref: webview-target-blank + ref: a37d157a9160fbd509484e9bb176be038c4d153c resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c url: "https://github.com/mateusz-ramp/flutter_packages.git" source: git version: "2.15.1" webview_flutter_wkwebview: - dependency: "direct overridden" + dependency: transitive description: path: "packages/webview_flutter/webview_flutter_wkwebview" - ref: webview-target-blank + ref: a37d157a9160fbd509484e9bb176be038c4d153c resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c url: "https://github.com/mateusz-ramp/flutter_packages.git" source: git diff --git a/example/pubspec.yaml b/example/pubspec.yaml index fb55d99..7be0890 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -17,29 +17,5 @@ dev_dependencies: sdk: flutter flutter_lints: ^6.0.0 -# Overrides only apply in the *root* package. Required when running the example. -# https://github.com/mateusz-ramp/flutter_packages/tree/webview-target-blank -dependency_overrides: - webview_flutter: - git: - url: https://github.com/mateusz-ramp/flutter_packages.git - ref: webview-target-blank - path: packages/webview_flutter/webview_flutter - webview_flutter_android: - git: - url: https://github.com/mateusz-ramp/flutter_packages.git - ref: webview-target-blank - path: packages/webview_flutter/webview_flutter_android - webview_flutter_wkwebview: - git: - url: https://github.com/mateusz-ramp/flutter_packages.git - ref: webview-target-blank - path: packages/webview_flutter/webview_flutter_wkwebview - webview_flutter_platform_interface: - git: - url: https://github.com/mateusz-ramp/flutter_packages.git - ref: webview-target-blank - path: packages/webview_flutter/webview_flutter_platform_interface - flutter: uses-material-design: true diff --git a/pubspec.lock b/pubspec.lock index a46c73b..17dfa34 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -473,7 +473,7 @@ packages: dependency: "direct main" description: path: "packages/webview_flutter/webview_flutter" - ref: webview-target-blank + ref: a37d157a9160fbd509484e9bb176be038c4d153c resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c url: "https://github.com/mateusz-ramp/flutter_packages.git" source: git @@ -482,16 +482,16 @@ packages: dependency: "direct main" description: path: "packages/webview_flutter/webview_flutter_android" - ref: webview-target-blank + ref: a37d157a9160fbd509484e9bb176be038c4d153c resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c url: "https://github.com/mateusz-ramp/flutter_packages.git" source: git version: "4.13.0" webview_flutter_platform_interface: - dependency: "direct overridden" + dependency: transitive description: path: "packages/webview_flutter/webview_flutter_platform_interface" - ref: webview-target-blank + ref: a37d157a9160fbd509484e9bb176be038c4d153c resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c url: "https://github.com/mateusz-ramp/flutter_packages.git" source: git @@ -500,7 +500,7 @@ packages: dependency: "direct main" description: path: "packages/webview_flutter/webview_flutter_wkwebview" - ref: webview-target-blank + ref: a37d157a9160fbd509484e9bb176be038c4d153c resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c url: "https://github.com/mateusz-ramp/flutter_packages.git" source: git diff --git a/pubspec.yaml b/pubspec.yaml index cd311a3..e289c52 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,36 +13,26 @@ dependencies: file_picker: ^10.1.2 image_picker: ^1.1.2 url_launcher: ^6.3.2 - webview_flutter: ^4.14.1 - webview_flutter_android: ^4.13.0 - webview_flutter_wkwebview: ^3.26.0 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^6.0.0 - -# Fork of flutter/packages with NavigationDelegate.onCreateWindow. -# Host apps must repeat these overrides (they only apply in the root package). -# https://github.com/mateusz-ramp/flutter_packages/tree/webview-target-blank -dependency_overrides: + # Fork with NavigationDelegate.onCreateWindow (target=_blank / window.open). + # Android / wkwebview are listed because we import them; same commit as webview_flutter. + # https://github.com/mateusz-ramp/flutter_packages/tree/webview-target-blank webview_flutter: git: url: https://github.com/mateusz-ramp/flutter_packages.git - ref: webview-target-blank + ref: a37d157a9160fbd509484e9bb176be038c4d153c path: packages/webview_flutter/webview_flutter webview_flutter_android: git: url: https://github.com/mateusz-ramp/flutter_packages.git - ref: webview-target-blank + ref: a37d157a9160fbd509484e9bb176be038c4d153c path: packages/webview_flutter/webview_flutter_android webview_flutter_wkwebview: git: url: https://github.com/mateusz-ramp/flutter_packages.git - ref: webview-target-blank + ref: a37d157a9160fbd509484e9bb176be038c4d153c path: packages/webview_flutter/webview_flutter_wkwebview - webview_flutter_platform_interface: - git: - url: https://github.com/mateusz-ramp/flutter_packages.git - ref: webview-target-blank - path: packages/webview_flutter/webview_flutter_platform_interface + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 From fe44358c4e0c2362e51d46afbda05f3b3d223d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 16:50:09 +0200 Subject: [PATCH 27/56] queue events --- CHANGELOG.md | 2 ++ README.md | 2 ++ example/README.md | 12 ++++++-- example/lib/main.dart | 3 -- lib/ramp_flutter.dart | 4 +++ lib/src/ramp_webview.dart | 57 +++++++++++++++++++++++++++++++++++-- test/ramp_webview_test.dart | 3 -- 7 files changed, 72 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4f265e..af64c47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ git dependency so hosts pick it up transitively. * Android WebView file inputs use `file_picker`; capture requests use `image_picker` (camera). +* Queue `postHostEvent` until the widget page has finished loading. +* Export configuration / events from `package:ramp_flutter/ramp_flutter.dart`. ## 4.0.1 diff --git a/README.md b/README.md index ea37852..7fe9c91 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ The SDK provides the Ramp WebView; your app owns presentation (route, bottom sheet, dialog, etc.) and must call `dispose` when it is dismissed. ```dart +import 'package:ramp_flutter/ramp_flutter.dart'; + final widgetUrl = Configuration( hostApiKey: 'YOUR_API_KEY', hostAppName: 'My App', diff --git a/example/README.md b/example/README.md index 6acbd7d..d0306f6 100644 --- a/example/README.md +++ b/example/README.md @@ -1,3 +1,11 @@ -# ramp_flutter_example +# Ramp Network Flutter example -A new Flutter project. +Demo app for [`ramp_flutter`](../): configure widget URL params, present the +Ramp WebView in a bottom sheet, and log `WidgetEvent` / host replies. + +```sh +flutter run +``` + +Copy `lib/secrets.example.dart` to `lib/secrets.dart` and set a host API key +for the internal/dev environment if needed. diff --git a/example/lib/main.dart b/example/lib/main.dart index c301964..a48f665 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,10 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; -import 'package:ramp_flutter/configuration.dart'; -import 'package:ramp_flutter/host_event.dart'; import 'package:ramp_flutter/ramp_flutter.dart'; -import 'package:ramp_flutter/widget_event.dart'; import 'secrets.dart'; diff --git a/lib/ramp_flutter.dart b/lib/ramp_flutter.dart index 9ee0de4..ff29a9b 100644 --- a/lib/ramp_flutter.dart +++ b/lib/ramp_flutter.dart @@ -4,6 +4,10 @@ import 'package:ramp_flutter/host_event.dart'; import 'package:ramp_flutter/src/ramp_webview.dart'; import 'package:ramp_flutter/widget_event.dart'; +export 'package:ramp_flutter/configuration.dart'; +export 'package:ramp_flutter/host_event.dart'; +export 'package:ramp_flutter/widget_event.dart'; + class RampFlutter { RampFlutter(Uri widgetUrl) : _webView = RampWebView(widgetUrl); diff --git a/lib/src/ramp_webview.dart b/lib/src/ramp_webview.dart index 8eeabac..11791dd 100644 --- a/lib/src/ramp_webview.dart +++ b/lib/src/ramp_webview.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:file_picker/file_picker.dart'; @@ -17,6 +18,8 @@ class RampWebView { final Uri _widgetUrl; WebViewController? _controller; + var _pageReady = false; + final _pendingHostEvents = <({HostEvent event, Completer done})>[]; void Function(WidgetEvent event)? onWidgetEvent; @@ -73,8 +76,15 @@ class RampWebView { } _openExternal(uri); }, - onPageStarted: (url) => debugPrint('RampFlutter: page started $url'), - onPageFinished: (url) => debugPrint('RampFlutter: page finished $url'), + onPageStarted: (url) { + _pageReady = false; + debugPrint('RampFlutter: page started $url'); + }, + onPageFinished: (url) { + debugPrint('RampFlutter: page finished $url'); + _pageReady = true; + _flushPendingHostEvents(); + }, onWebResourceError: (error) { debugPrint( 'RampFlutter: resource error ' @@ -214,13 +224,54 @@ class RampWebView { } Future postHostEvent(HostEvent event) { - final webView = _ensureController(); + _ensureController(); + if (_pageReady) { + return _sendHostEvent(event); + } + debugPrint('RampFlutter: queue postHostEvent ${event.type} until page ready'); + final done = Completer(); + _pendingHostEvents.add((event: event, done: done)); + return done.future; + } + + Future _sendHostEvent(HostEvent event) { + final webView = _controller; + if (webView == null) { + return Future.value(); + } final message = jsonEncode(event.toJson()); debugPrint('RampFlutter: postHostEvent ${event.type}'); return webView.runJavaScript('window.postMessage($message, "${_widgetUrl.origin}");'); } + Future _flushPendingHostEvents() async { + if (_pendingHostEvents.isEmpty) { + return; + } + final pending = List.of(_pendingHostEvents); + _pendingHostEvents.clear(); + for (final item in pending) { + try { + await _sendHostEvent(item.event); + if (!item.done.isCompleted) { + item.done.complete(); + } + } catch (error, stackTrace) { + if (!item.done.isCompleted) { + item.done.completeError(error, stackTrace); + } + } + } + } + void dispose() { + for (final item in _pendingHostEvents) { + if (!item.done.isCompleted) { + item.done.complete(); + } + } + _pendingHostEvents.clear(); + _pageReady = false; _controller ?..removeJavaScriptChannel(_channelName) ..loadRequest(Uri.parse('about:blank')); diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index 8bf8ac4..2269714 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -1,10 +1,7 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; -import 'package:ramp_flutter/configuration.dart'; -import 'package:ramp_flutter/host_event.dart'; import 'package:ramp_flutter/ramp_flutter.dart'; -import 'package:ramp_flutter/widget_event.dart'; void main() { group('Configuration.buildWidgetUrl', () { From 9d2f70021697b25dce65a93420548108618fd6aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 16:52:12 +0200 Subject: [PATCH 28/56] rename files --- CHANGELOG.md | 2 +- lib/ramp_flutter.dart | 12 ++++---- lib/{ => src}/configuration.dart | 0 lib/{ => src}/host_event.dart | 0 .../request_crypto_account_result.dart | 0 .../host_events/send_crypto_result.dart | 0 lib/{ => src}/models/asset_info.dart | 2 +- lib/{ => src}/models/json_map.dart | 0 lib/{ => src}/models/purchase_details.dart | 4 +-- lib/{ => src}/models/sale_details.dart | 4 +-- lib/src/ramp_webview.dart | 4 +-- lib/{ => src}/widget_event.dart | 30 +++++++++---------- .../widget_events}/offramp_sale_created.dart | 0 .../widget_events}/purchase_created.dart | 0 .../request_crypto_account.dart | 0 .../widget_events}/send_crypto_requested.dart | 0 .../widget_events}/widget_close.dart | 0 .../widget_events}/widget_close_request.dart | 0 .../widget_events}/widget_config_done.dart | 0 .../widget_events}/widget_config_failed.dart | 0 20 files changed, 29 insertions(+), 29 deletions(-) rename lib/{ => src}/configuration.dart (100%) rename lib/{ => src}/host_event.dart (100%) rename lib/{ => src}/host_events/request_crypto_account_result.dart (100%) rename lib/{ => src}/host_events/send_crypto_result.dart (100%) rename lib/{ => src}/models/asset_info.dart (93%) rename lib/{ => src}/models/json_map.dart (100%) rename lib/{ => src}/models/purchase_details.dart (95%) rename lib/{ => src}/models/sale_details.dart (95%) rename lib/{ => src}/widget_event.dart (76%) rename lib/{events => src/widget_events}/offramp_sale_created.dart (100%) rename lib/{events => src/widget_events}/purchase_created.dart (100%) rename lib/{events => src/widget_events}/request_crypto_account.dart (100%) rename lib/{events => src/widget_events}/send_crypto_requested.dart (100%) rename lib/{events => src/widget_events}/widget_close.dart (100%) rename lib/{events => src/widget_events}/widget_close_request.dart (100%) rename lib/{events => src/widget_events}/widget_config_done.dart (100%) rename lib/{events => src/widget_events}/widget_config_failed.dart (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index af64c47..9f4bfaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ * Android WebView file inputs use `file_picker`; capture requests use `image_picker` (camera). * Queue `postHostEvent` until the widget page has finished loading. -* Export configuration / events from `package:ramp_flutter/ramp_flutter.dart`. +* Public API is `package:ramp_flutter/ramp_flutter.dart` only (implementation under `lib/src/`). ## 4.0.1 diff --git a/lib/ramp_flutter.dart b/lib/ramp_flutter.dart index ff29a9b..6f0af81 100644 --- a/lib/ramp_flutter.dart +++ b/lib/ramp_flutter.dart @@ -1,12 +1,12 @@ import 'package:flutter/widgets.dart'; -import 'package:ramp_flutter/configuration.dart'; -import 'package:ramp_flutter/host_event.dart'; +import 'package:ramp_flutter/src/configuration.dart'; +import 'package:ramp_flutter/src/host_event.dart'; import 'package:ramp_flutter/src/ramp_webview.dart'; -import 'package:ramp_flutter/widget_event.dart'; +import 'package:ramp_flutter/src/widget_event.dart'; -export 'package:ramp_flutter/configuration.dart'; -export 'package:ramp_flutter/host_event.dart'; -export 'package:ramp_flutter/widget_event.dart'; +export 'package:ramp_flutter/src/configuration.dart'; +export 'package:ramp_flutter/src/host_event.dart'; +export 'package:ramp_flutter/src/widget_event.dart'; class RampFlutter { RampFlutter(Uri widgetUrl) : _webView = RampWebView(widgetUrl); diff --git a/lib/configuration.dart b/lib/src/configuration.dart similarity index 100% rename from lib/configuration.dart rename to lib/src/configuration.dart diff --git a/lib/host_event.dart b/lib/src/host_event.dart similarity index 100% rename from lib/host_event.dart rename to lib/src/host_event.dart diff --git a/lib/host_events/request_crypto_account_result.dart b/lib/src/host_events/request_crypto_account_result.dart similarity index 100% rename from lib/host_events/request_crypto_account_result.dart rename to lib/src/host_events/request_crypto_account_result.dart diff --git a/lib/host_events/send_crypto_result.dart b/lib/src/host_events/send_crypto_result.dart similarity index 100% rename from lib/host_events/send_crypto_result.dart rename to lib/src/host_events/send_crypto_result.dart diff --git a/lib/models/asset_info.dart b/lib/src/models/asset_info.dart similarity index 93% rename from lib/models/asset_info.dart rename to lib/src/models/asset_info.dart index 75b79af..41e9ee4 100644 --- a/lib/models/asset_info.dart +++ b/lib/src/models/asset_info.dart @@ -1,4 +1,4 @@ -import 'package:ramp_flutter/models/json_map.dart'; +import 'package:ramp_flutter/src/models/json_map.dart'; class AssetInfo { const AssetInfo({ diff --git a/lib/models/json_map.dart b/lib/src/models/json_map.dart similarity index 100% rename from lib/models/json_map.dart rename to lib/src/models/json_map.dart diff --git a/lib/models/purchase_details.dart b/lib/src/models/purchase_details.dart similarity index 95% rename from lib/models/purchase_details.dart rename to lib/src/models/purchase_details.dart index ae67877..3520b9a 100644 --- a/lib/models/purchase_details.dart +++ b/lib/src/models/purchase_details.dart @@ -1,5 +1,5 @@ -import 'package:ramp_flutter/models/asset_info.dart'; -import 'package:ramp_flutter/models/json_map.dart'; +import 'package:ramp_flutter/src/models/asset_info.dart'; +import 'package:ramp_flutter/src/models/json_map.dart'; class PurchaseDetails { const PurchaseDetails({ diff --git a/lib/models/sale_details.dart b/lib/src/models/sale_details.dart similarity index 95% rename from lib/models/sale_details.dart rename to lib/src/models/sale_details.dart index fbb0dde..500d7ea 100644 --- a/lib/models/sale_details.dart +++ b/lib/src/models/sale_details.dart @@ -1,5 +1,5 @@ -import 'package:ramp_flutter/models/asset_info.dart'; -import 'package:ramp_flutter/models/json_map.dart'; +import 'package:ramp_flutter/src/models/asset_info.dart'; +import 'package:ramp_flutter/src/models/json_map.dart'; class SaleDetails { const SaleDetails({this.id, this.createdAt, this.updatedAt, this.crypto, this.fiat, this.fees, this.exchangeRate}); diff --git a/lib/src/ramp_webview.dart b/lib/src/ramp_webview.dart index 11791dd..6fae258 100644 --- a/lib/src/ramp_webview.dart +++ b/lib/src/ramp_webview.dart @@ -4,8 +4,8 @@ import 'dart:convert'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/widgets.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:ramp_flutter/host_event.dart'; -import 'package:ramp_flutter/widget_event.dart'; +import 'package:ramp_flutter/src/host_event.dart'; +import 'package:ramp_flutter/src/widget_event.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter_android/webview_flutter_android.dart'; diff --git a/lib/widget_event.dart b/lib/src/widget_event.dart similarity index 76% rename from lib/widget_event.dart rename to lib/src/widget_event.dart index e7b0078..b988af5 100644 --- a/lib/widget_event.dart +++ b/lib/src/widget_event.dart @@ -1,21 +1,21 @@ import 'package:flutter/foundation.dart'; -import 'package:ramp_flutter/models/asset_info.dart'; -import 'package:ramp_flutter/models/json_map.dart'; -import 'package:ramp_flutter/models/purchase_details.dart'; -import 'package:ramp_flutter/models/sale_details.dart'; +import 'package:ramp_flutter/src/models/asset_info.dart'; +import 'package:ramp_flutter/src/models/json_map.dart'; +import 'package:ramp_flutter/src/models/purchase_details.dart'; +import 'package:ramp_flutter/src/models/sale_details.dart'; -export 'package:ramp_flutter/models/asset_info.dart'; -export 'package:ramp_flutter/models/purchase_details.dart'; -export 'package:ramp_flutter/models/sale_details.dart'; +export 'package:ramp_flutter/src/models/asset_info.dart'; +export 'package:ramp_flutter/src/models/purchase_details.dart'; +export 'package:ramp_flutter/src/models/sale_details.dart'; -part 'events/offramp_sale_created.dart'; -part 'events/purchase_created.dart'; -part 'events/request_crypto_account.dart'; -part 'events/send_crypto_requested.dart'; -part 'events/widget_close.dart'; -part 'events/widget_close_request.dart'; -part 'events/widget_config_done.dart'; -part 'events/widget_config_failed.dart'; +part 'widget_events/offramp_sale_created.dart'; +part 'widget_events/purchase_created.dart'; +part 'widget_events/request_crypto_account.dart'; +part 'widget_events/send_crypto_requested.dart'; +part 'widget_events/widget_close.dart'; +part 'widget_events/widget_close_request.dart'; +part 'widget_events/widget_config_done.dart'; +part 'widget_events/widget_config_failed.dart'; sealed class WidgetEvent { const WidgetEvent({this.widgetInstanceId}); diff --git a/lib/events/offramp_sale_created.dart b/lib/src/widget_events/offramp_sale_created.dart similarity index 100% rename from lib/events/offramp_sale_created.dart rename to lib/src/widget_events/offramp_sale_created.dart diff --git a/lib/events/purchase_created.dart b/lib/src/widget_events/purchase_created.dart similarity index 100% rename from lib/events/purchase_created.dart rename to lib/src/widget_events/purchase_created.dart diff --git a/lib/events/request_crypto_account.dart b/lib/src/widget_events/request_crypto_account.dart similarity index 100% rename from lib/events/request_crypto_account.dart rename to lib/src/widget_events/request_crypto_account.dart diff --git a/lib/events/send_crypto_requested.dart b/lib/src/widget_events/send_crypto_requested.dart similarity index 100% rename from lib/events/send_crypto_requested.dart rename to lib/src/widget_events/send_crypto_requested.dart diff --git a/lib/events/widget_close.dart b/lib/src/widget_events/widget_close.dart similarity index 100% rename from lib/events/widget_close.dart rename to lib/src/widget_events/widget_close.dart diff --git a/lib/events/widget_close_request.dart b/lib/src/widget_events/widget_close_request.dart similarity index 100% rename from lib/events/widget_close_request.dart rename to lib/src/widget_events/widget_close_request.dart diff --git a/lib/events/widget_config_done.dart b/lib/src/widget_events/widget_config_done.dart similarity index 100% rename from lib/events/widget_config_done.dart rename to lib/src/widget_events/widget_config_done.dart diff --git a/lib/events/widget_config_failed.dart b/lib/src/widget_events/widget_config_failed.dart similarity index 100% rename from lib/events/widget_config_failed.dart rename to lib/src/widget_events/widget_config_failed.dart From b2b3ed4f22b92c919c7b729b5f927bd072ce64a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 16:56:26 +0200 Subject: [PATCH 29/56] extract webview tools --- lib/src/android_file_selector.dart | 76 ++++++++++++++++++ lib/src/external_navigation.dart | 41 ++++++++++ lib/src/ramp_webview.dart | 121 ++--------------------------- 3 files changed, 123 insertions(+), 115 deletions(-) create mode 100644 lib/src/android_file_selector.dart create mode 100644 lib/src/external_navigation.dart diff --git a/lib/src/android_file_selector.dart b/lib/src/android_file_selector.dart new file mode 100644 index 0000000..1be63ad --- /dev/null +++ b/lib/src/android_file_selector.dart @@ -0,0 +1,76 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:webview_flutter_android/webview_flutter_android.dart'; + +Future> showAndroidFileSelector(FileSelectorParams params) async { + try { + if (params.isCaptureEnabled) { + final photo = await ImagePicker().pickImage(source: ImageSource.camera); + if (photo == null) { + return const []; + } + return [Uri.file(photo.path).toString()]; + } + + final fileType = fileTypeForAcceptTypes(params.acceptTypes); + final result = await FilePicker.platform.pickFiles( + allowMultiple: params.mode == FileSelectorMode.openMultiple, + type: fileType, + allowedExtensions: fileType == FileType.custom ? extensionsForAcceptTypes(params.acceptTypes) : null, + ); + if (result == null) { + return const []; + } + return [ + for (final file in result.files) + if (file.path != null) Uri.file(file.path!).toString(), + ]; + } catch (error, stackTrace) { + debugPrint('RampFlutter: file selection failed: $error\n$stackTrace'); + return const []; + } +} + +@visibleForTesting +FileType fileTypeForAcceptTypes(List acceptTypes) { + if (acceptTypes.isEmpty) { + return FileType.any; + } + final normalized = acceptTypes.map((type) => type.toLowerCase().trim()).toList(); + final onlyImages = normalized.every((type) => type.startsWith('image/')); + if (onlyImages) { + return FileType.image; + } + final onlyVideos = normalized.every((type) => type.startsWith('video/')); + if (onlyVideos) { + return FileType.video; + } + final extensions = extensionsForAcceptTypes(acceptTypes); + final allMappedToExtensions = normalized.every( + (type) => type.startsWith('.') || type == 'application/pdf', + ); + if (allMappedToExtensions && extensions != null && extensions.isNotEmpty) { + return FileType.custom; + } + return FileType.any; +} + +@visibleForTesting +List? extensionsForAcceptTypes(List acceptTypes) { + final extensions = {}; + for (final type in acceptTypes) { + final value = type.trim().toLowerCase(); + if (value.startsWith('.')) { + extensions.add(value.substring(1)); + continue; + } + if (value == 'application/pdf') { + extensions.add('pdf'); + } + } + if (extensions.isEmpty) { + return null; + } + return extensions.toList(); +} diff --git a/lib/src/external_navigation.dart b/lib/src/external_navigation.dart new file mode 100644 index 0000000..3cb4937 --- /dev/null +++ b/lib/src/external_navigation.dart @@ -0,0 +1,41 @@ +import 'package:flutter/foundation.dart'; +import 'package:url_launcher/url_launcher.dart'; + +bool shouldOpenExternally(Uri uri, Uri widgetUrl) { + final scheme = uri.scheme.toLowerCase(); + final isHttp = scheme == 'http' || scheme == 'https'; + if (!isHttp) { + return true; + } + return uri.host.toLowerCase() != widgetUrl.host.toLowerCase(); +} + +Future openExternalUrl(Uri uri) async { + try { + if (uri.scheme == 'intent') { + final fallback = intentFallbackUrl(uri); + if (fallback != null) { + await launchUrl(fallback, mode: LaunchMode.externalApplication); + } else { + debugPrint('RampFlutter: intent URL has no browser_fallback_url: $uri'); + } + return; + } + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + debugPrint('RampFlutter: cannot launch URL $uri'); + } + } catch (error, stackTrace) { + debugPrint('RampFlutter: failed to open $uri: $error\n$stackTrace'); + } +} + +@visibleForTesting +Uri? intentFallbackUrl(Uri intentUri) { + final browserFallback = intentUri.queryParameters['browser_fallback_url']; + if (browserFallback != null && browserFallback.isNotEmpty) { + return Uri.tryParse(browserFallback); + } + return null; +} diff --git a/lib/src/ramp_webview.dart b/lib/src/ramp_webview.dart index 6fae258..eada8db 100644 --- a/lib/src/ramp_webview.dart +++ b/lib/src/ramp_webview.dart @@ -1,12 +1,11 @@ import 'dart:async'; import 'dart:convert'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter/widgets.dart'; -import 'package:image_picker/image_picker.dart'; +import 'package:ramp_flutter/src/android_file_selector.dart'; +import 'package:ramp_flutter/src/external_navigation.dart'; import 'package:ramp_flutter/src/host_event.dart'; import 'package:ramp_flutter/src/widget_event.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter_android/webview_flutter_android.dart'; import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; @@ -59,9 +58,9 @@ class RampWebView { debugPrint('RampFlutter: block navigation — invalid URL ${request.url}'); return NavigationDecision.prevent; } - if (_shouldOpenExternally(uri)) { + if (shouldOpenExternally(uri, _widgetUrl)) { debugPrint('RampFlutter: open external (navigation) ${request.url}'); - _openExternal(uri); + openExternalUrl(uri); return NavigationDecision.prevent; } debugPrint('RampFlutter: allow navigation ${request.url}'); @@ -74,7 +73,7 @@ class RampWebView { debugPrint('RampFlutter: create window ignored — invalid URL $url'); return; } - _openExternal(uri); + openExternalUrl(uri); }, onPageStarted: (url) { _pageReady = false; @@ -108,121 +107,13 @@ class RampWebView { final platform = controller.platform; if (platform is AndroidWebViewController) { platform.setMediaPlaybackRequiresUserGesture(false); - platform.setOnShowFileSelector(_androidFileSelector); + platform.setOnShowFileSelector(showAndroidFileSelector); } controller.loadRequest(_widgetUrl); return controller; } - Future> _androidFileSelector(FileSelectorParams params) async { - try { - if (params.isCaptureEnabled) { - final photo = await ImagePicker().pickImage(source: ImageSource.camera); - if (photo == null) { - return const []; - } - return [Uri.file(photo.path).toString()]; - } - - final fileType = _fileTypeForAcceptTypes(params.acceptTypes); - final result = await FilePicker.platform.pickFiles( - allowMultiple: params.mode == FileSelectorMode.openMultiple, - type: fileType, - allowedExtensions: fileType == FileType.custom ? _extensionsForAcceptTypes(params.acceptTypes) : null, - ); - if (result == null) { - return const []; - } - return [ - for (final file in result.files) - if (file.path != null) Uri.file(file.path!).toString(), - ]; - } catch (error, stackTrace) { - debugPrint('RampFlutter: file selection failed: $error\n$stackTrace'); - return const []; - } - } - - static FileType _fileTypeForAcceptTypes(List acceptTypes) { - if (acceptTypes.isEmpty) { - return FileType.any; - } - final normalized = acceptTypes.map((type) => type.toLowerCase().trim()).toList(); - final onlyImages = normalized.every((type) => type.startsWith('image/')); - if (onlyImages) { - return FileType.image; - } - final onlyVideos = normalized.every((type) => type.startsWith('video/')); - if (onlyVideos) { - return FileType.video; - } - final extensions = _extensionsForAcceptTypes(acceptTypes); - final allMappedToExtensions = normalized.every( - (type) => type.startsWith('.') || type == 'application/pdf', - ); - if (allMappedToExtensions && extensions != null && extensions.isNotEmpty) { - return FileType.custom; - } - return FileType.any; - } - - static List? _extensionsForAcceptTypes(List acceptTypes) { - final extensions = {}; - for (final type in acceptTypes) { - final value = type.trim().toLowerCase(); - if (value.startsWith('.')) { - extensions.add(value.substring(1)); - continue; - } - if (value == 'application/pdf') { - extensions.add('pdf'); - } - } - if (extensions.isEmpty) { - return null; - } - return extensions.toList(); - } - - bool _shouldOpenExternally(Uri uri) { - final scheme = uri.scheme.toLowerCase(); - final isHttp = scheme == 'http' || scheme == 'https'; - if (!isHttp) { - return true; - } - return uri.host.toLowerCase() != _widgetUrl.host.toLowerCase(); - } - - Future _openExternal(Uri uri) async { - try { - if (uri.scheme == 'intent') { - final fallback = _intentFallbackUrl(uri); - if (fallback != null) { - await launchUrl(fallback, mode: LaunchMode.externalApplication); - } else { - debugPrint('RampFlutter: intent URL has no browser_fallback_url: $uri'); - } - return; - } - if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); - } else { - debugPrint('RampFlutter: cannot launch URL $uri'); - } - } catch (error, stackTrace) { - debugPrint('RampFlutter: failed to open $uri: $error\n$stackTrace'); - } - } - - static Uri? _intentFallbackUrl(Uri intentUri) { - final browserFallback = intentUri.queryParameters['browser_fallback_url']; - if (browserFallback != null && browserFallback.isNotEmpty) { - return Uri.tryParse(browserFallback); - } - return null; - } - Future postHostEvent(HostEvent event) { _ensureController(); if (_pageReady) { From 03e4b3c997fb1a0e21af7f40cc5dd8f02ec2dcc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 17:02:37 +0200 Subject: [PATCH 30/56] store example deplocks --- .../xcshareddata/swiftpm/Package.resolved | 59 +++++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 59 +++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..4d7193e --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,59 @@ +{ + "pins" : [ + { + "identity" : "dkcamera", + "kind" : "remoteSourceControl", + "location" : "https://github.com/zhangao0086/DKCamera", + "state" : { + "branch" : "master", + "revision" : "5c691d11014b910aff69f960475d70e65d9dcc96" + } + }, + { + "identity" : "dkimagepickercontroller", + "kind" : "remoteSourceControl", + "location" : "https://github.com/zhangao0086/DKImagePickerController", + "state" : { + "branch" : "4.3.9", + "revision" : "0bdfeacefa308545adde07bef86e349186335915" + } + }, + { + "identity" : "dkphotogallery", + "kind" : "remoteSourceControl", + "location" : "https://github.com/zhangao0086/DKPhotoGallery", + "state" : { + "branch" : "master", + "revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d" + } + }, + { + "identity" : "sdwebimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SDWebImage/SDWebImage", + "state" : { + "revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0", + "version" : "5.21.7" + } + }, + { + "identity" : "swiftygif", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kirualex/SwiftyGif.git", + "state" : { + "revision" : "4430cbc148baa3907651d40562d96325426f409a", + "version" : "5.4.5" + } + }, + { + "identity" : "tocropviewcontroller", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TimOliver/TOCropViewController", + "state" : { + "revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e", + "version" : "2.8.0" + } + } + ], + "version" : 2 +} diff --git a/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..4d7193e --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,59 @@ +{ + "pins" : [ + { + "identity" : "dkcamera", + "kind" : "remoteSourceControl", + "location" : "https://github.com/zhangao0086/DKCamera", + "state" : { + "branch" : "master", + "revision" : "5c691d11014b910aff69f960475d70e65d9dcc96" + } + }, + { + "identity" : "dkimagepickercontroller", + "kind" : "remoteSourceControl", + "location" : "https://github.com/zhangao0086/DKImagePickerController", + "state" : { + "branch" : "4.3.9", + "revision" : "0bdfeacefa308545adde07bef86e349186335915" + } + }, + { + "identity" : "dkphotogallery", + "kind" : "remoteSourceControl", + "location" : "https://github.com/zhangao0086/DKPhotoGallery", + "state" : { + "branch" : "master", + "revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d" + } + }, + { + "identity" : "sdwebimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SDWebImage/SDWebImage", + "state" : { + "revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0", + "version" : "5.21.7" + } + }, + { + "identity" : "swiftygif", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kirualex/SwiftyGif.git", + "state" : { + "revision" : "4430cbc148baa3907651d40562d96325426f409a", + "version" : "5.4.5" + } + }, + { + "identity" : "tocropviewcontroller", + "kind" : "remoteSourceControl", + "location" : "https://github.com/TimOliver/TOCropViewController", + "state" : { + "revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e", + "version" : "2.8.0" + } + } + ], + "version" : 2 +} From 920ccf7c806e7e26b4b651cd63b0c9e5a7b1fd8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 17:04:42 +0200 Subject: [PATCH 31/56] fix empty schemes --- lib/src/external_navigation.dart | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/src/external_navigation.dart b/lib/src/external_navigation.dart index 3cb4937..c857143 100644 --- a/lib/src/external_navigation.dart +++ b/lib/src/external_navigation.dart @@ -3,11 +3,28 @@ import 'package:url_launcher/url_launcher.dart'; bool shouldOpenExternally(Uri uri, Uri widgetUrl) { final scheme = uri.scheme.toLowerCase(); - final isHttp = scheme == 'http' || scheme == 'https'; - if (!isHttp) { + if (scheme == 'about' || scheme.isEmpty) { + return false; + } + if (scheme != 'http' && scheme != 'https') { + return true; + } + if (uri.host.toLowerCase() == widgetUrl.host.toLowerCase()) { + return false; + } + return !staysInWebView(uri); +} + +@visibleForTesting +bool staysInWebView(Uri uri) { + final host = uri.host.toLowerCase(); + if (host == 'recaptcha.net' || host.endsWith('.recaptcha.net')) { return true; } - return uri.host.toLowerCase() != widgetUrl.host.toLowerCase(); + if (host == 'www.google.com' || host == 'www.gstatic.com') { + return uri.path.toLowerCase().contains('/recaptcha'); + } + return false; } Future openExternalUrl(Uri uri) async { From c3973654a3d12d9290896453fef602164fb92768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 17:11:57 +0200 Subject: [PATCH 32/56] remove verbose logging --- example/lib/main.dart | 1 - lib/src/ramp_webview.dart | 15 ++------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index a48f665..647657a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -83,7 +83,6 @@ class _RampFlutterAppState extends State { void _addDebugEvent(String label, [Map data = const {}]) { final encoded = const JsonEncoder.withIndent(' ').convert({'event': label, ...data}); - debugPrint('Ramp example event:\n$encoded'); _debugEvents.value = [..._debugEvents.value, _DebugEvent(_nextDebugEventId++, encoded)]; } diff --git a/lib/src/ramp_webview.dart b/lib/src/ramp_webview.dart index eada8db..eb1d810 100644 --- a/lib/src/ramp_webview.dart +++ b/lib/src/ramp_webview.dart @@ -34,8 +34,6 @@ class RampWebView { params = const PlatformWebViewControllerCreationParams(); } - debugPrint('RampFlutter: loading $_widgetUrl'); - final controller = WebViewController.fromPlatformCreationParams( params, @@ -59,15 +57,12 @@ class RampWebView { return NavigationDecision.prevent; } if (shouldOpenExternally(uri, _widgetUrl)) { - debugPrint('RampFlutter: open external (navigation) ${request.url}'); openExternalUrl(uri); return NavigationDecision.prevent; } - debugPrint('RampFlutter: allow navigation ${request.url}'); return NavigationDecision.navigate; }, onCreateWindow: (url) { - debugPrint('RampFlutter: open external (create window) $url'); final uri = Uri.tryParse(url); if (uri == null) { debugPrint('RampFlutter: create window ignored — invalid URL $url'); @@ -75,12 +70,10 @@ class RampWebView { } openExternalUrl(uri); }, - onPageStarted: (url) { + onPageStarted: (_) { _pageReady = false; - debugPrint('RampFlutter: page started $url'); }, - onPageFinished: (url) { - debugPrint('RampFlutter: page finished $url'); + onPageFinished: (_) { _pageReady = true; _flushPendingHostEvents(); }, @@ -119,7 +112,6 @@ class RampWebView { if (_pageReady) { return _sendHostEvent(event); } - debugPrint('RampFlutter: queue postHostEvent ${event.type} until page ready'); final done = Completer(); _pendingHostEvents.add((event: event, done: done)); return done.future; @@ -131,7 +123,6 @@ class RampWebView { return Future.value(); } final message = jsonEncode(event.toJson()); - debugPrint('RampFlutter: postHostEvent ${event.type}'); return webView.runJavaScript('window.postMessage($message, "${_widgetUrl.origin}");'); } @@ -174,11 +165,9 @@ class RampWebView { try { decoded = jsonDecode(message); } on FormatException catch (error) { - debugPrint('RampFlutter: drop JS message — invalid JSON: $error'); return; } if (decoded is! Map) { - debugPrint('RampFlutter: drop JS message — expected Map, got ${decoded.runtimeType}'); return; } final event = WidgetEvent.tryParse(Map.from(decoded)); From ab0ba3756e8c0b379d4696f7365e16120e91dc0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 17:23:10 +0200 Subject: [PATCH 33/56] streamline widget creation --- CHANGELOG.md | 15 ++++++---- README.md | 32 ++++++++++++-------- example/lib/main.dart | 2 +- lib/ramp_flutter.dart | 11 +++++-- lib/src/configuration.dart | 8 +++-- lib/src/ramp_webview.dart | 2 +- lib/src/signed_url.dart | 16 ++++++++++ test/ramp_webview_test.dart | 6 ++-- test/signed_url_test.dart | 59 +++++++++++++++++++++++++++++++++++++ 9 files changed, 123 insertions(+), 28 deletions(-) create mode 100644 lib/src/signed_url.dart create mode 100644 test/signed_url_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f4bfaf..10908f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,15 @@ * Rewrite the SDK to load the Ramp widget in a Flutter WebView instead of the native iOS/Android Ramp SDKs. Published as a Dart Flutter package (no Android/iOS plugin shells). -* Breaking: SDK no longer presents UI. Create `RampFlutter(widgetUrl)` (or - `RampFlutter.fromConfiguration`), embed `ramp.view` in your own route/sheet, - and call `dispose` when done. `Configuration` is an immutable helper that - only builds the widget URL. +* Breaking: SDK no longer presents UI. Create `RampFlutter(Configuration)` or + `RampFlutter.signed(url)`, embed `ramp.view` in your own route/sheet, and + call `dispose` when done. `Configuration` is an immutable helper used only + for the config-built entry point. * Widget → host: sealed `WidgetEvent` via `onWidgetEvent` (includes `WidgetCloseRequest`). Host → widget: sealed `HostEvent` (`SendCryptoResult`, `RequestCryptoAccountResult`) via `postHostEvent`. -* Add `Configuration.offrampAsset` and `Configuration.buildWidgetUrl`, including - `sdkType` / `sdkVersion` / `variant`. +* Add `Configuration.offrampAsset` and URL building (including `sdkType` / + `sdkVersion` / `variant`) inside `RampFlutter(Configuration)`. * Default base URL is now `https://app.rampnetwork.com`. * Raise minimum platforms to Android 7.0 (API 24) and iOS 13. * Raise minimum Flutter to 3.44 / Dart 3.12; update `webview_flutter`, @@ -26,6 +26,9 @@ `image_picker` (camera). * Queue `postHostEvent` until the widget page has finished loading. * Public API is `package:ramp_flutter/ramp_flutter.dart` only (implementation under `lib/src/`). +* Add `RampFlutter.signed` for server-signed widget URLs (loaded verbatim). + Config URL building rejects bases that already include a `signature` query + parameter. ## 4.0.1 diff --git a/README.md b/README.md index 7fe9c91..5441114 100644 --- a/README.md +++ b/README.md @@ -28,17 +28,20 @@ dependencies: The SDK provides the Ramp WebView; your app owns presentation (route, bottom sheet, dialog, etc.) and must call `dispose` when it is dismissed. +**Configuration** (SDK builds the widget URL, including `sdkType` / `sdkVersion` / +`variant`): + ```dart import 'package:ramp_flutter/ramp_flutter.dart'; -final widgetUrl = Configuration( - hostApiKey: 'YOUR_API_KEY', - hostAppName: 'My App', - hostLogoUrl: 'https://example.com/logo.png', - enabledFlows: ['ONRAMP', 'OFFRAMP'], -).buildWidgetUrl(); - -final ramp = RampFlutter(widgetUrl) +final ramp = RampFlutter( + Configuration( + hostApiKey: 'YOUR_API_KEY', + hostAppName: 'My App', + hostLogoUrl: 'https://example.com/logo.png', + enabledFlows: ['ONRAMP', 'OFFRAMP'], + ), +) ..onWidgetEvent = (event) { switch (event) { case PurchaseCreated(:final payload): @@ -75,8 +78,15 @@ await showModalBottomSheet( ramp.dispose(); ``` -`Configuration` only builds the widget [Uri]; `RampFlutter` takes that URL (or use -`RampFlutter.fromConfiguration(...)`). +**Server-signed URL** (loaded verbatim — do not put this in `Configuration.url`): + +```dart +final ramp = RampFlutter.signed(signedWidgetUrlFromYourBackend) + ..onWidgetEvent = (event) { /* same switch as above */ }; +``` + +Generate the signed URL on your backend (`hostApiKey`, `timestamp`, `signature`). +Keep signing keys out of the app. For more configuration parameters see [Ramp Network Flutter documentation](https://docs.ramp.network/mobile/flutter-sdk/). @@ -94,5 +104,3 @@ For more configuration parameters see `image_picker` when capture is requested). Host apps need camera / photo library usage descriptions (see Getting Started). iOS WKWebView handles file inputs natively. -- Server-signed widget URLs are not supported in this release; use - `Configuration` fields so the SDK can build the widget URL. diff --git a/example/lib/main.dart b/example/lib/main.dart index 647657a..92fa864 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -91,7 +91,7 @@ class _RampFlutterAppState extends State { } Future _showRamp(BuildContext context) async { - final ramp = RampFlutter.fromConfiguration(_buildConfiguration()); + final ramp = RampFlutter(_buildConfiguration()); ramp.onWidgetEvent = (event) { switch (event) { diff --git a/lib/ramp_flutter.dart b/lib/ramp_flutter.dart index 6f0af81..e4f57de 100644 --- a/lib/ramp_flutter.dart +++ b/lib/ramp_flutter.dart @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import 'package:ramp_flutter/src/configuration.dart'; import 'package:ramp_flutter/src/host_event.dart'; import 'package:ramp_flutter/src/ramp_webview.dart'; +import 'package:ramp_flutter/src/signed_url.dart'; import 'package:ramp_flutter/src/widget_event.dart'; export 'package:ramp_flutter/src/configuration.dart'; @@ -9,10 +10,14 @@ export 'package:ramp_flutter/src/host_event.dart'; export 'package:ramp_flutter/src/widget_event.dart'; class RampFlutter { - RampFlutter(Uri widgetUrl) : _webView = RampWebView(widgetUrl); + RampFlutter(Configuration configuration) : this._(configuration.buildWidgetUrl()); - factory RampFlutter.fromConfiguration(Configuration configuration) => - RampFlutter(configuration.buildWidgetUrl()); + factory RampFlutter.signed(String url) => RampFlutter._(validateRampSignedUrl(url)); + + @visibleForTesting + factory RampFlutter.uri(Uri widgetUrl) => RampFlutter._(widgetUrl); + + RampFlutter._(Uri widgetUrl) : _webView = RampWebView(widgetUrl); final RampWebView _webView; diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index d43d723..4c5bfe8 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -1,5 +1,3 @@ -/// Builds a Ramp widget [Uri]. Not held by [RampFlutter] after construction — -/// call [buildWidgetUrl] and pass the result into [RampFlutter]. class Configuration { const Configuration({ this.url, @@ -54,6 +52,12 @@ class Configuration { Uri buildWidgetUrl() { final base = Uri.parse((url != null && url!.trim().isNotEmpty) ? url!.trim() : defaultUrl); + if (base.queryParameters['signature']?.isNotEmpty == true) { + throw StateError( + 'Configuration.buildWidgetUrl() cannot be used with a signed URL. ' + 'Use RampFlutter.signed(...) instead.', + ); + } final queryParameters = { ...base.queryParameters, diff --git a/lib/src/ramp_webview.dart b/lib/src/ramp_webview.dart index eb1d810..8b94cb8 100644 --- a/lib/src/ramp_webview.dart +++ b/lib/src/ramp_webview.dart @@ -164,7 +164,7 @@ class RampWebView { final dynamic decoded; try { decoded = jsonDecode(message); - } on FormatException catch (error) { + } on FormatException { return; } if (decoded is! Map) { diff --git a/lib/src/signed_url.dart b/lib/src/signed_url.dart new file mode 100644 index 0000000..f43adf8 --- /dev/null +++ b/lib/src/signed_url.dart @@ -0,0 +1,16 @@ +final _trustedRampHost = RegExp( + r'^([a-z0-9-]+\.)*(ramp\.network|rampnetwork\.com|ramp-network\.org)$', +); + +Uri validateRampSignedUrl(String value) { + final url = Uri.tryParse(value); + final parameters = url?.queryParameters ?? const {}; + const requiredParameters = ['hostApiKey', 'timestamp', 'signature']; + if (url == null || + url.scheme != 'https' || + !_trustedRampHost.hasMatch(url.host) || + !requiredParameters.every((parameter) => parameters[parameter]?.isNotEmpty == true)) { + throw ArgumentError.value(value, 'url', 'Invalid signed Ramp Network URL'); + } + return url; +} diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index 2269714..befd4a6 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -48,7 +48,7 @@ void main() { test('ignores non-JSON and unknown types', () { final events = []; - final ramp = RampFlutter(widgetUrl)..onWidgetEvent = events.add; + final ramp = RampFlutter.uri(widgetUrl)..onWidgetEvent = events.add; ramp.handleJavaScriptMessage('not json'); ramp.handleJavaScriptMessage('42'); @@ -59,7 +59,7 @@ void main() { test('parses supported widget events', () { final events = []; - final ramp = RampFlutter(widgetUrl)..onWidgetEvent = events.add; + final ramp = RampFlutter.uri(widgetUrl)..onWidgetEvent = events.add; ramp.handleJavaScriptMessage( jsonEncode({'type': 'WIDGET_CONFIG_DONE', 'payload': null, 'widgetInstanceId': 'w1'}), @@ -175,7 +175,7 @@ void main() { test('rejects unsupported SEND_CRYPTO eventVersion', () { final events = []; - final ramp = RampFlutter(widgetUrl)..onWidgetEvent = events.add; + final ramp = RampFlutter.uri(widgetUrl)..onWidgetEvent = events.add; ramp.handleJavaScriptMessage( jsonEncode({ diff --git a/test/signed_url_test.dart b/test/signed_url_test.dart new file mode 100644 index 0000000..8f5f2f9 --- /dev/null +++ b/test/signed_url_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:ramp_flutter/ramp_flutter.dart'; +import 'package:ramp_flutter/src/signed_url.dart'; + +void main() { + const signedUrl = + 'https://app.ramp.network/?hostApiKey=key×tamp=123&signature=a%2Bb%3D%3D'; + + test('preserves the signed URL', () { + expect(validateRampSignedUrl(signedUrl).toString(), signedUrl); + }); + + test('rejects an incomplete signed URL', () { + expect( + () => validateRampSignedUrl( + 'https://app.ramp.network/?hostApiKey=key×tamp=123', + ), + throwsArgumentError, + ); + }); + + test('rejects an untrusted origin', () { + expect( + () => validateRampSignedUrl( + 'https://app.ramp.network.evil.example/' + '?hostApiKey=key×tamp=123&signature=value', + ), + throwsArgumentError, + ); + }); + + test('accepts trusted Ramp hosts', () { + expect( + validateRampSignedUrl( + 'https://app.dev.ramp-network.org/?hostApiKey=key×tamp=123&signature=sig', + ).host, + 'app.dev.ramp-network.org', + ); + expect( + validateRampSignedUrl( + 'https://app.rampnetwork.com/?hostApiKey=key×tamp=123&signature=sig', + ).host, + 'app.rampnetwork.com', + ); + }); + + test('signed constructs RampFlutter', () { + final ramp = RampFlutter.signed(signedUrl); + expect(ramp, isA()); + ramp.dispose(); + }); + + test('buildWidgetUrl rejects signed base URLs', () { + expect( + () => Configuration(url: signedUrl).buildWidgetUrl(), + throwsStateError, + ); + }); +} From 06285ece578813d9f6eabe70f87c845b49aae960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 17:24:28 +0200 Subject: [PATCH 34/56] flatten webview indent --- lib/src/ramp_webview.dart | 157 +++++++++++++++++++++----------------- 1 file changed, 85 insertions(+), 72 deletions(-) diff --git a/lib/src/ramp_webview.dart b/lib/src/ramp_webview.dart index 8b94cb8..da4cedd 100644 --- a/lib/src/ramp_webview.dart +++ b/lib/src/ramp_webview.dart @@ -27,84 +27,97 @@ class RampWebView { WebViewController _ensureController() => _controller ??= _createController(); WebViewController _createController() { - final PlatformWebViewControllerCreationParams params; + final controller = WebViewController.fromPlatformCreationParams( + _platformParams(), + onPermissionRequest: _onPermissionRequest, + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(_navigationDelegate()) + ..addJavaScriptChannel( + _channelName, + onMessageReceived: (message) => handleJavaScriptMessage(message.message), + ); + + _configureAndroid(controller); + controller.loadRequest(_widgetUrl); + return controller; + } + + PlatformWebViewControllerCreationParams _platformParams() { if (WebViewPlatform.instance is WebKitWebViewPlatform) { - params = WebKitWebViewControllerCreationParams(allowsInlineMediaPlayback: true); - } else { - params = const PlatformWebViewControllerCreationParams(); + return WebKitWebViewControllerCreationParams(allowsInlineMediaPlayback: true); } + return const PlatformWebViewControllerCreationParams(); + } - final controller = - WebViewController.fromPlatformCreationParams( - params, - onPermissionRequest: (request) { - final onlyCamera = request.types.every((type) => type == WebViewPermissionResourceType.camera); - if (onlyCamera) { - request.grant(); - } else { - debugPrint('RampFlutter: deny WebView permission types=${request.types}'); - request.deny(); - } - }, - ) - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate( - NavigationDelegate( - onNavigationRequest: (request) { - final uri = Uri.tryParse(request.url); - if (uri == null) { - debugPrint('RampFlutter: block navigation — invalid URL ${request.url}'); - return NavigationDecision.prevent; - } - if (shouldOpenExternally(uri, _widgetUrl)) { - openExternalUrl(uri); - return NavigationDecision.prevent; - } - return NavigationDecision.navigate; - }, - onCreateWindow: (url) { - final uri = Uri.tryParse(url); - if (uri == null) { - debugPrint('RampFlutter: create window ignored — invalid URL $url'); - return; - } - openExternalUrl(uri); - }, - onPageStarted: (_) { - _pageReady = false; - }, - onPageFinished: (_) { - _pageReady = true; - _flushPendingHostEvents(); - }, - onWebResourceError: (error) { - debugPrint( - 'RampFlutter: resource error ' - 'code=${error.errorCode} type=${error.errorType} ' - 'desc=${error.description} url=${error.url}', - ); - }, - onHttpError: (error) { - debugPrint( - 'RampFlutter: HTTP error ' - 'status=${error.response?.statusCode} uri=${error.request?.uri}', - ); - }, - ), - ) - ..addJavaScriptChannel( - _channelName, - onMessageReceived: (message) => handleJavaScriptMessage(message.message), - ); + NavigationDelegate _navigationDelegate() { + return NavigationDelegate( + onNavigationRequest: _onNavigationRequest, + onCreateWindow: _onCreateWindow, + onPageStarted: (_) => _pageReady = false, + onPageFinished: (_) { + _pageReady = true; + _flushPendingHostEvents(); + }, + onWebResourceError: _onWebResourceError, + onHttpError: _onHttpError, + ); + } - final platform = controller.platform; - if (platform is AndroidWebViewController) { - platform.setMediaPlaybackRequiresUserGesture(false); - platform.setOnShowFileSelector(showAndroidFileSelector); + void _onPermissionRequest(WebViewPermissionRequest request) { + final onlyCamera = request.types.every((type) => type == WebViewPermissionResourceType.camera); + if (onlyCamera) { + request.grant(); + return; } + debugPrint('RampFlutter: deny WebView permission types=${request.types}'); + request.deny(); + } - controller.loadRequest(_widgetUrl); - return controller; + NavigationDecision _onNavigationRequest(NavigationRequest request) { + final uri = Uri.tryParse(request.url); + if (uri == null) { + debugPrint('RampFlutter: block navigation — invalid URL ${request.url}'); + return NavigationDecision.prevent; + } + if (shouldOpenExternally(uri, _widgetUrl)) { + openExternalUrl(uri); + return NavigationDecision.prevent; + } + return NavigationDecision.navigate; + } + + void _onCreateWindow(String url) { + final uri = Uri.tryParse(url); + if (uri == null) { + debugPrint('RampFlutter: create window ignored — invalid URL $url'); + return; + } + openExternalUrl(uri); + } + + void _onWebResourceError(WebResourceError error) { + debugPrint( + 'RampFlutter: resource error ' + 'code=${error.errorCode} type=${error.errorType} ' + 'desc=${error.description} url=${error.url}', + ); + } + + void _onHttpError(HttpResponseError error) { + debugPrint( + 'RampFlutter: HTTP error ' + 'status=${error.response?.statusCode} uri=${error.request?.uri}', + ); + } + + void _configureAndroid(WebViewController controller) { + final platform = controller.platform; + if (platform is! AndroidWebViewController) { + return; + } + platform.setMediaPlaybackRequiresUserGesture(false); + platform.setOnShowFileSelector(showAndroidFileSelector); } Future postHostEvent(HostEvent event) { From cbccd34e8b37c39830805fdf3c4e81c954b137f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 17:28:12 +0200 Subject: [PATCH 35/56] reformat --- analysis_options.yaml | 1 + example/analysis_options.yaml | 1 + example/lib/main.dart | 16 +++---------- lib/src/android_file_selector.dart | 4 +--- .../request_crypto_account_result.dart | 21 +++-------------- lib/src/host_events/send_crypto_result.dart | 6 ++--- lib/src/models/asset_info.dart | 10 +------- lib/src/ramp_webview.dart | 18 +++++++-------- lib/src/signed_url.dart | 4 +--- lib/src/widget_event.dart | 20 ++++------------ .../widget_events/request_crypto_account.dart | 5 +--- test/signed_url_test.dart | 23 ++++--------------- 12 files changed, 31 insertions(+), 98 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index 76940e6..29f8cb3 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,3 +2,4 @@ include: package:flutter_lints/flutter.yaml formatter: page_width: 120 + diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 76940e6..29f8cb3 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -2,3 +2,4 @@ include: package:flutter_lints/flutter.yaml formatter: page_width: 120 + diff --git a/example/lib/main.dart b/example/lib/main.dart index 92fa864..14cbe09 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -100,10 +100,7 @@ class _RampFlutterAppState extends State { case WidgetConfigFailed(): _addDebugEvent('WIDGET_CONFIG_FAILED'); case PurchaseCreated(:final payload): - _addDebugEvent('PURCHASE_CREATED', { - 'id': payload.purchase?.id, - 'asset': payload.purchase?.asset?.symbol, - }); + _addDebugEvent('PURCHASE_CREATED', {'id': payload.purchase?.id, 'asset': payload.purchase?.asset?.symbol}); case OfframpSaleCreated(:final payload): _addDebugEvent('OFFRAMP_SALE_CREATED', {'id': payload.sale?.id}); case SendCryptoRequested(:final payload): @@ -114,16 +111,9 @@ class _RampFlutterAppState extends State { }); ramp.postHostEvent(SendCryptoResult.txHash('123')); case RequestCryptoAccount(:final payload): - _addDebugEvent('REQUEST_CRYPTO_ACCOUNT', { - 'type': payload.type, - 'assetSymbol': payload.assetSymbol, - }); + _addDebugEvent('REQUEST_CRYPTO_ACCOUNT', {'type': payload.type, 'assetSymbol': payload.assetSymbol}); ramp.postHostEvent( - RequestCryptoAccountResult.account( - address: '0xabc', - type: payload.type, - assetSymbol: payload.assetSymbol, - ), + RequestCryptoAccountResult.account(address: '0xabc', type: payload.type, assetSymbol: payload.assetSymbol), ); case WidgetClose(:final payload): _addDebugEvent('WIDGET_CLOSE', {'showAlert': payload.showAlert}); diff --git a/lib/src/android_file_selector.dart b/lib/src/android_file_selector.dart index 1be63ad..955c2c3 100644 --- a/lib/src/android_file_selector.dart +++ b/lib/src/android_file_selector.dart @@ -47,9 +47,7 @@ FileType fileTypeForAcceptTypes(List acceptTypes) { return FileType.video; } final extensions = extensionsForAcceptTypes(acceptTypes); - final allMappedToExtensions = normalized.every( - (type) => type.startsWith('.') || type == 'application/pdf', - ); + final allMappedToExtensions = normalized.every((type) => type.startsWith('.') || type == 'application/pdf'); if (allMappedToExtensions && extensions != null && extensions.isNotEmpty) { return FileType.custom; } diff --git a/lib/src/host_events/request_crypto_account_result.dart b/lib/src/host_events/request_crypto_account_result.dart index 5576024..b28c5df 100644 --- a/lib/src/host_events/request_crypto_account_result.dart +++ b/lib/src/host_events/request_crypto_account_result.dart @@ -9,12 +9,7 @@ final class RequestCryptoAccountResult extends HostEvent { String? name, String? assetSymbol, }) => RequestCryptoAccountResult( - RequestCryptoAccountSuccessPayload( - address: address, - type: type, - name: name, - assetSymbol: assetSymbol, - ), + RequestCryptoAccountSuccessPayload(address: address, type: type, name: name, assetSymbol: assetSymbol), ); factory RequestCryptoAccountResult.error([String? error]) => @@ -36,12 +31,7 @@ sealed class RequestCryptoAccountResultPayload { } final class RequestCryptoAccountSuccessPayload extends RequestCryptoAccountResultPayload { - const RequestCryptoAccountSuccessPayload({ - required this.address, - this.type, - this.name, - this.assetSymbol, - }); + const RequestCryptoAccountSuccessPayload({required this.address, this.type, this.name, this.assetSymbol}); final String address; final String? type; @@ -49,12 +39,7 @@ final class RequestCryptoAccountSuccessPayload extends RequestCryptoAccountResul final String? assetSymbol; @override - Map toJson() => { - 'address': address, - 'type': ?type, - 'name': ?name, - 'assetSymbol': ?assetSymbol, - }; + Map toJson() => {'address': address, 'type': ?type, 'name': ?name, 'assetSymbol': ?assetSymbol}; } final class RequestCryptoAccountErrorPayload extends RequestCryptoAccountResultPayload { diff --git a/lib/src/host_events/send_crypto_result.dart b/lib/src/host_events/send_crypto_result.dart index 99f9f35..7cd6f11 100644 --- a/lib/src/host_events/send_crypto_result.dart +++ b/lib/src/host_events/send_crypto_result.dart @@ -3,11 +3,9 @@ part of '../host_event.dart'; final class SendCryptoResult extends HostEvent { const SendCryptoResult(this.payload); - factory SendCryptoResult.txHash(String? txHash) => - SendCryptoResult(SendCryptoResultTxHashPayload(txHash)); + factory SendCryptoResult.txHash(String? txHash) => SendCryptoResult(SendCryptoResultTxHashPayload(txHash)); - factory SendCryptoResult.error([String? error]) => - SendCryptoResult(SendCryptoResultErrorPayload(error)); + factory SendCryptoResult.error([String? error]) => SendCryptoResult(SendCryptoResultErrorPayload(error)); final SendCryptoResultPayload payload; diff --git a/lib/src/models/asset_info.dart b/lib/src/models/asset_info.dart index 41e9ee4..8a4cf99 100644 --- a/lib/src/models/asset_info.dart +++ b/lib/src/models/asset_info.dart @@ -1,15 +1,7 @@ import 'package:ramp_flutter/src/models/json_map.dart'; class AssetInfo { - const AssetInfo({ - this.uai, - this.address, - this.symbol, - this.chain, - this.type, - this.name, - this.decimals, - }); + const AssetInfo({this.uai, this.address, this.symbol, this.chain, this.type, this.name, this.decimals}); final String? uai; final String? address; diff --git a/lib/src/ramp_webview.dart b/lib/src/ramp_webview.dart index da4cedd..e9523a4 100644 --- a/lib/src/ramp_webview.dart +++ b/lib/src/ramp_webview.dart @@ -27,16 +27,14 @@ class RampWebView { WebViewController _ensureController() => _controller ??= _createController(); WebViewController _createController() { - final controller = WebViewController.fromPlatformCreationParams( - _platformParams(), - onPermissionRequest: _onPermissionRequest, - ) - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setNavigationDelegate(_navigationDelegate()) - ..addJavaScriptChannel( - _channelName, - onMessageReceived: (message) => handleJavaScriptMessage(message.message), - ); + final controller = + WebViewController.fromPlatformCreationParams(_platformParams(), onPermissionRequest: _onPermissionRequest) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate(_navigationDelegate()) + ..addJavaScriptChannel( + _channelName, + onMessageReceived: (message) => handleJavaScriptMessage(message.message), + ); _configureAndroid(controller); controller.loadRequest(_widgetUrl); diff --git a/lib/src/signed_url.dart b/lib/src/signed_url.dart index f43adf8..8548945 100644 --- a/lib/src/signed_url.dart +++ b/lib/src/signed_url.dart @@ -1,6 +1,4 @@ -final _trustedRampHost = RegExp( - r'^([a-z0-9-]+\.)*(ramp\.network|rampnetwork\.com|ramp-network\.org)$', -); +final _trustedRampHost = RegExp(r'^([a-z0-9-]+\.)*(ramp\.network|rampnetwork\.com|ramp-network\.org)$'); Uri validateRampSignedUrl(String value) { final url = Uri.tryParse(value); diff --git a/lib/src/widget_event.dart b/lib/src/widget_event.dart index b988af5..1409ff2 100644 --- a/lib/src/widget_event.dart +++ b/lib/src/widget_event.dart @@ -38,15 +38,9 @@ sealed class WidgetEvent { case 'WIDGET_CONFIG_FAILED': return WidgetConfigFailed(widgetInstanceId: widgetInstanceId); case 'PURCHASE_CREATED': - return PurchaseCreated( - PurchaseCreatedPayload.fromJson(payload), - widgetInstanceId: widgetInstanceId, - ); + return PurchaseCreated(PurchaseCreatedPayload.fromJson(payload), widgetInstanceId: widgetInstanceId); case 'OFFRAMP_SALE_CREATED': - return OfframpSaleCreated( - OfframpSaleCreatedPayload.fromJson(payload), - widgetInstanceId: widgetInstanceId, - ); + return OfframpSaleCreated(OfframpSaleCreatedPayload.fromJson(payload), widgetInstanceId: widgetInstanceId); case 'SEND_CRYPTO': final version = json['eventVersion']; if (version != null && version != 1) { @@ -57,10 +51,7 @@ sealed class WidgetEvent { debugPrint('RampFlutter: drop SEND_CRYPTO — missing payload'); return null; } - return SendCryptoRequested( - SendCryptoPayload.fromJson(payload), - widgetInstanceId: widgetInstanceId, - ); + return SendCryptoRequested(SendCryptoPayload.fromJson(payload), widgetInstanceId: widgetInstanceId); case 'REQUEST_CRYPTO_ACCOUNT': if (payload == null) { debugPrint('RampFlutter: drop REQUEST_CRYPTO_ACCOUNT — missing payload'); @@ -72,10 +63,7 @@ sealed class WidgetEvent { ); case 'WIDGET_CLOSE': case 'CLOSE': - return WidgetClose( - payload: WidgetClosePayload.fromJson(payload), - widgetInstanceId: widgetInstanceId, - ); + return WidgetClose(payload: WidgetClosePayload.fromJson(payload), widgetInstanceId: widgetInstanceId); case 'WIDGET_CLOSE_REQUEST': return WidgetCloseRequest(widgetInstanceId: widgetInstanceId); default: diff --git a/lib/src/widget_events/request_crypto_account.dart b/lib/src/widget_events/request_crypto_account.dart index f5ad2bf..ffc09a3 100644 --- a/lib/src/widget_events/request_crypto_account.dart +++ b/lib/src/widget_events/request_crypto_account.dart @@ -16,9 +16,6 @@ class RequestCryptoAccountPayload { if (json == null) { return const RequestCryptoAccountPayload(); } - return RequestCryptoAccountPayload( - type: json['type'] as String?, - assetSymbol: json['assetSymbol'] as String?, - ); + return RequestCryptoAccountPayload(type: json['type'] as String?, assetSymbol: json['assetSymbol'] as String?); } } diff --git a/test/signed_url_test.dart b/test/signed_url_test.dart index 8f5f2f9..47d8197 100644 --- a/test/signed_url_test.dart +++ b/test/signed_url_test.dart @@ -3,20 +3,14 @@ import 'package:ramp_flutter/ramp_flutter.dart'; import 'package:ramp_flutter/src/signed_url.dart'; void main() { - const signedUrl = - 'https://app.ramp.network/?hostApiKey=key×tamp=123&signature=a%2Bb%3D%3D'; + const signedUrl = 'https://app.ramp.network/?hostApiKey=key×tamp=123&signature=a%2Bb%3D%3D'; test('preserves the signed URL', () { expect(validateRampSignedUrl(signedUrl).toString(), signedUrl); }); test('rejects an incomplete signed URL', () { - expect( - () => validateRampSignedUrl( - 'https://app.ramp.network/?hostApiKey=key×tamp=123', - ), - throwsArgumentError, - ); + expect(() => validateRampSignedUrl('https://app.ramp.network/?hostApiKey=key×tamp=123'), throwsArgumentError); }); test('rejects an untrusted origin', () { @@ -31,15 +25,11 @@ void main() { test('accepts trusted Ramp hosts', () { expect( - validateRampSignedUrl( - 'https://app.dev.ramp-network.org/?hostApiKey=key×tamp=123&signature=sig', - ).host, + validateRampSignedUrl('https://app.dev.ramp-network.org/?hostApiKey=key×tamp=123&signature=sig').host, 'app.dev.ramp-network.org', ); expect( - validateRampSignedUrl( - 'https://app.rampnetwork.com/?hostApiKey=key×tamp=123&signature=sig', - ).host, + validateRampSignedUrl('https://app.rampnetwork.com/?hostApiKey=key×tamp=123&signature=sig').host, 'app.rampnetwork.com', ); }); @@ -51,9 +41,6 @@ void main() { }); test('buildWidgetUrl rejects signed base URLs', () { - expect( - () => Configuration(url: signedUrl).buildWidgetUrl(), - throwsStateError, - ); + expect(() => Configuration(url: signedUrl).buildWidgetUrl(), throwsStateError); }); } From 5b299dd4d332130ba9c3607d0b3e9b539fc48269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 17:36:53 +0200 Subject: [PATCH 36/56] Ignore example/lib/secrets.dart so local API keys stay untracked. Co-authored-by: Cursor --- .gitignore | 3 +++ example/.gitignore | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index eb6c05c..3275458 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ migrate_working_dir/ .flutter-plugins .flutter-plugins-dependencies build/ + +# Example local secrets (copy from secrets.example.dart) +example/lib/secrets.dart diff --git a/example/.gitignore b/example/.gitignore index 3820a95..73b00db 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -43,3 +43,6 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# Local secrets (copy from secrets.example.dart) +lib/secrets.dart From 9e5fa2d7a63e17e980b282b9d0940d6a151d7a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 17:41:15 +0200 Subject: [PATCH 37/56] remove unused test files --- .gitignore | 3 - example/.gitignore | 3 - example/README.md | 3 - example/ios/Runner.xcodeproj/project.pbxproj | 183 +----------------- .../xcshareddata/xcschemes/Runner.xcscheme | 11 -- example/ios/RunnerTests/RunnerTests.swift | 12 -- example/lib/main.dart | 3 - example/lib/secrets.example.dart | 4 - example/pubspec.yaml | 2 - 9 files changed, 1 insertion(+), 223 deletions(-) delete mode 100644 example/ios/RunnerTests/RunnerTests.swift delete mode 100644 example/lib/secrets.example.dart diff --git a/.gitignore b/.gitignore index 3275458..eb6c05c 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,3 @@ migrate_working_dir/ .flutter-plugins .flutter-plugins-dependencies build/ - -# Example local secrets (copy from secrets.example.dart) -example/lib/secrets.dart diff --git a/example/.gitignore b/example/.gitignore index 73b00db..3820a95 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -43,6 +43,3 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release - -# Local secrets (copy from secrets.example.dart) -lib/secrets.dart diff --git a/example/README.md b/example/README.md index d0306f6..f7ea7d8 100644 --- a/example/README.md +++ b/example/README.md @@ -6,6 +6,3 @@ Ramp WebView in a bottom sheet, and log `WidgetEvent` / host replies. ```sh flutter run ``` - -Copy `lib/secrets.example.dart` to `lib/secrets.dart` and set a host API key -for the internal/dev environment if needed. diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 75b34a0..e87f5fe 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,24 +8,6 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -44,43 +26,9 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -99,7 +47,6 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - 331C8082294A63A400263BE5 /* RunnerTests */, ); sourceTree = ""; }; @@ -107,7 +54,6 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -131,23 +77,6 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C807D294A63A400263BE5 /* Sources */, - 331C807F294A63A400263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C8086294A63A400263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; @@ -181,46 +110,9 @@ LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { - 331C8080294A63A400263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, - ); - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - 331C8080294A63A400263BE5 /* RunnerTests */, - ); - }; -/* End PBXProject section */ + /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 331C807F294A63A400263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -269,14 +161,6 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 331C807D294A63A400263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -289,14 +173,6 @@ }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -391,53 +267,6 @@ }; name = Profile; }; - 331C8088294A63A400263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = network.ramp.rampFlutterExample.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Debug; - }; - 331C8089294A63A400263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = network.ramp.rampFlutterExample.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Release; - }; - 331C808A294A63A400263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = network.ramp.rampFlutterExample.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Profile; - }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -597,16 +426,6 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C8088294A63A400263BE5 /* Debug */, - 331C8089294A63A400263BE5 /* Release */, - 331C808A294A63A400263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c3fedb2..5db441f 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -56,17 +56,6 @@ - - - - { void _applyEnvironment(int id) { _selectedEnvironment = id; - _hostApiKey = id == 0 ? ExampleSecrets.hostApiKeyInternal : null; } Configuration _buildConfiguration() { diff --git a/example/lib/secrets.example.dart b/example/lib/secrets.example.dart deleted file mode 100644 index 92e8c6d..0000000 --- a/example/lib/secrets.example.dart +++ /dev/null @@ -1,4 +0,0 @@ -/// Copy to [secrets.dart] and fill in values. Do not commit secrets.dart. -class ExampleSecrets { - static const String hostApiKeyInternal = 'YOUR_HOST_API_KEY_INTERNAL'; -} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 7be0890..783e8f2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -13,8 +13,6 @@ dependencies: path: ../ dev_dependencies: - flutter_test: - sdk: flutter flutter_lints: ^6.0.0 flutter: From f9f061ca0ff87ccb9bd02a84c74468d8ff777871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 17:41:46 +0200 Subject: [PATCH 38/56] update dependencies --- example/pubspec.lock | 93 -------------------------------------------- 1 file changed, 93 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index ab8e65f..0e41e9b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -17,14 +17,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" characters: dependency: transitive description: @@ -33,14 +25,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" collection: dependency: transitive description: @@ -65,14 +49,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.14" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" ffi: dependency: transitive description: @@ -142,11 +118,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.35" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -232,30 +203,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" lints: dependency: transitive description: @@ -264,14 +211,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 - url: "https://pub.dev" - source: hosted - version: "0.12.19" material_color_utilities: dependency: transitive description: @@ -340,22 +279,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" string_scanner: dependency: transitive description: @@ -372,14 +295,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" - url: "https://pub.dev" - source: hosted - version: "0.7.11" typed_data: dependency: transitive description: @@ -460,14 +375,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" - url: "https://pub.dev" - source: hosted - version: "15.2.0" web: dependency: transitive description: From aad75562eb3367fc2b2b581e0b607c057e5107a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 17:51:26 +0200 Subject: [PATCH 39/56] adapt params to existing widget options --- CHANGELOG.md | 7 ++++-- README.md | 5 ++-- analysis_options.yaml | 1 - example/analysis_options.yaml | 1 - example/lib/main.dart | 34 +++++++++++++++----------- lib/src/configuration.dart | 45 +++++++++++++---------------------- test/ramp_webview_test.dart | 18 +++++++++----- 7 files changed, 55 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10908f2..8401a42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,11 @@ * Widget → host: sealed `WidgetEvent` via `onWidgetEvent` (includes `WidgetCloseRequest`). Host → widget: sealed `HostEvent` (`SendCryptoResult`, `RequestCryptoAccountResult`) via `postHostEvent`. -* Add `Configuration.offrampAsset` and URL building (including `sdkType` / - `sdkVersion` / `variant`) inside `RampFlutter(Configuration)`. +* `Configuration` builds widget URLs with widget-2 params (`inAsset` / + `outAsset` / `inAssetValue` / `outAssetValue` / `enabledCryptoAssets`, plus + `sdkType` / `sdkVersion`). Legacy params (`defaultAsset`, `fiatCurrency`, + `fiatValue`, `swapAsset`, `offrampAsset`, `swapAmount`, `hostLogoUrl`, + `containerNode`, `deepLinkScheme`, `variant`) are removed. * Default base URL is now `https://app.rampnetwork.com`. * Raise minimum platforms to Android 7.0 (API 24) and iOS 13. * Raise minimum Flutter to 3.44 / Dart 3.12; update `webview_flutter`, diff --git a/README.md b/README.md index 5441114..9f61168 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,7 @@ dependencies: The SDK provides the Ramp WebView; your app owns presentation (route, bottom sheet, dialog, etc.) and must call `dispose` when it is dismissed. -**Configuration** (SDK builds the widget URL, including `sdkType` / `sdkVersion` / -`variant`): +**Configuration** (SDK builds the widget URL, including `sdkType` / `sdkVersion`): ```dart import 'package:ramp_flutter/ramp_flutter.dart'; @@ -38,8 +37,8 @@ final ramp = RampFlutter( Configuration( hostApiKey: 'YOUR_API_KEY', hostAppName: 'My App', - hostLogoUrl: 'https://example.com/logo.png', enabledFlows: ['ONRAMP', 'OFFRAMP'], + outAsset: 'BTC_BTC', ), ) ..onWidgetEvent = (event) { diff --git a/analysis_options.yaml b/analysis_options.yaml index 29f8cb3..76940e6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,4 +2,3 @@ include: package:flutter_lints/flutter.yaml formatter: page_width: 120 - diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 29f8cb3..76940e6 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -2,4 +2,3 @@ include: package:flutter_lints/flutter.yaml formatter: page_width: 120 - diff --git a/example/lib/main.dart b/example/lib/main.dart index ff914ff..e6377c3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -28,10 +28,11 @@ class _RampFlutterAppState extends State { int _selectedEnvironment = 0; String? _userEmailAddress; - String? _fiatValue; - String? _fiatCurrency; - String? _defaultAsset = 'BTC_BTC'; - String? _offrampAsset; + String? _inAsset; + String? _inAssetValue; + String? _outAsset = 'BTC_BTC'; + String? _outAssetValue; + String? _enabledCryptoAssets; String? _userAddress; String? _hostAppName = 'Ramp Network Flutter'; String? _hostApiKey; @@ -60,20 +61,24 @@ class _RampFlutterAppState extends State { } Configuration _buildConfiguration() { + final enabledCryptoAssets = _enabledCryptoAssets + ?.split(',') + .map((asset) => asset.trim()) + .where((asset) => asset.isNotEmpty) + .toList(); return Configuration( url: _predefinedEnvironments[_selectedEnvironment], hostAppName: _hostAppName, - hostLogoUrl: 'https://assets.rampnetwork.com/misc/ramp-network-logo.svg', defaultFlow: _defaultFlow, enabledFlows: List.from(_enabledFlows), - defaultAsset: _defaultAsset, - offrampAsset: _offrampAsset, + enabledCryptoAssets: enabledCryptoAssets, + inAsset: _inAsset, + inAssetValue: _inAssetValue, + outAsset: _outAsset, + outAssetValue: _outAssetValue, useSendCryptoCallback: true, - deepLinkScheme: 'rampflutterdemo', hostApiKey: _hostApiKey, userEmailAddress: _userEmailAddress, - fiatValue: _fiatValue, - fiatCurrency: _fiatCurrency, userAddress: _userAddress, ); } @@ -215,10 +220,11 @@ class _RampFlutterAppState extends State { style: const TextStyle(color: Color.fromRGBO(46, 190, 117, 1)), ), _textField('User email address', (text) => _userEmailAddress = text, _userEmailAddress), - _textField('Fiat value', (text) => _fiatValue = text, _fiatValue), - _textField('Fiat currency', (text) => _fiatCurrency = text, _fiatCurrency), - _textField('Default asset', (text) => _defaultAsset = text, _defaultAsset), - _textField('Offramp asset', (text) => _offrampAsset = text, _offrampAsset), + _textField('In asset', (text) => _inAsset = text, _inAsset), + _textField('In asset value', (text) => _inAssetValue = text, _inAssetValue), + _textField('Out asset', (text) => _outAsset = text, _outAsset), + _textField('Out asset value', (text) => _outAssetValue = text, _outAssetValue), + _textField('Enabled crypto assets', (text) => _enabledCryptoAssets = text, _enabledCryptoAssets), _textField('User address', (text) => _userAddress = text, _userAddress), _textField('Host app name', (text) => _hostAppName = text, _hostAppName), _textField('Host API key', (text) => _hostApiKey = text, _hostApiKey), diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index 4c5bfe8..9af4c60 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -1,22 +1,18 @@ class Configuration { const Configuration({ this.url, - this.containerNode, - this.deepLinkScheme, - this.defaultAsset, this.defaultFlow, this.enabledFlows, - this.fiatCurrency, - this.fiatValue, + this.enabledCryptoAssets, + this.inAsset, + this.inAssetValue, + this.outAsset, + this.outAssetValue, this.finalUrl, this.hostApiKey, this.hostAppName, - this.hostLogoUrl, - this.offrampAsset, this.offrampWebhookV3Url, this.selectedCountryCode, - this.swapAmount, - this.swapAsset, this.userAddress, this.userEmailAddress, this.useSendCryptoCallback, @@ -26,25 +22,20 @@ class Configuration { static const String defaultUrl = 'https://app.rampnetwork.com'; static const String sdkType = 'FLUTTER'; static const String sdkVersion = '5.0.0'; - static const String mobileSdkVariant = 'sdk-mobile'; final String? url; - final String? containerNode; - final String? deepLinkScheme; - final String? defaultAsset; final String? defaultFlow; final List? enabledFlows; - final String? fiatCurrency; - final String? fiatValue; + final List? enabledCryptoAssets; + final String? inAsset; + final String? inAssetValue; + final String? outAsset; + final String? outAssetValue; final String? finalUrl; final String? hostApiKey; final String? hostAppName; - final String? hostLogoUrl; - final String? offrampAsset; final String? offrampWebhookV3Url; final String? selectedCountryCode; - final String? swapAmount; - final String? swapAsset; final String? userAddress; final String? userEmailAddress; final bool? useSendCryptoCallback; @@ -61,28 +52,24 @@ class Configuration { final queryParameters = { ...base.queryParameters, - if (_nonEmpty(containerNode)) 'containerNode': containerNode!, - if (_nonEmpty(deepLinkScheme)) 'deepLinkScheme': deepLinkScheme!, - if (_nonEmpty(defaultAsset)) 'defaultAsset': defaultAsset!, if (_nonEmpty(defaultFlow)) 'defaultFlow': defaultFlow!, if (enabledFlows != null && enabledFlows!.isNotEmpty) 'enabledFlows': enabledFlows!.join(','), - if (_nonEmpty(fiatCurrency)) 'fiatCurrency': fiatCurrency!, - if (_nonEmpty(fiatValue)) 'fiatValue': fiatValue!, + if (enabledCryptoAssets != null && enabledCryptoAssets!.isNotEmpty) + 'enabledCryptoAssets': enabledCryptoAssets!.join(','), + if (_nonEmpty(inAsset)) 'inAsset': inAsset!, + if (_nonEmpty(inAssetValue)) 'inAssetValue': inAssetValue!, + if (_nonEmpty(outAsset)) 'outAsset': outAsset!, + if (_nonEmpty(outAssetValue)) 'outAssetValue': outAssetValue!, if (_nonEmpty(finalUrl)) 'finalUrl': finalUrl!, if (_nonEmpty(hostApiKey)) 'hostApiKey': hostApiKey!, if (_nonEmpty(hostAppName)) 'hostAppName': hostAppName!, - if (_nonEmpty(hostLogoUrl)) 'hostLogoUrl': hostLogoUrl!, - if (_nonEmpty(offrampAsset)) 'offrampAsset': offrampAsset!, if (_nonEmpty(offrampWebhookV3Url)) 'offrampWebhookV3Url': offrampWebhookV3Url!, if (_nonEmpty(selectedCountryCode)) 'selectedCountryCode': selectedCountryCode!, - if (_nonEmpty(swapAmount)) 'swapAmount': swapAmount!, - if (_nonEmpty(swapAsset)) 'swapAsset': swapAsset!, if (_nonEmpty(userAddress)) 'userAddress': userAddress!, if (_nonEmpty(userEmailAddress)) 'userEmailAddress': userEmailAddress!, if (_nonEmpty(webhookStatusUrl)) 'webhookStatusUrl': webhookStatusUrl!, 'sdkType': sdkType, 'sdkVersion': sdkVersion, - 'variant': mobileSdkVariant, }; if (useSendCryptoCallback == true) { diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index befd4a6..71f1750 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -12,15 +12,18 @@ void main() { expect(url.host, 'app.rampnetwork.com'); expect(url.queryParameters['sdkType'], 'FLUTTER'); expect(url.queryParameters['sdkVersion'], '5.0.0'); - expect(url.queryParameters['variant'], 'sdk-mobile'); + expect(url.queryParameters.containsKey('variant'), isFalse); }); - test('merges configuration fields and joins enabled flows', () { + test('merges configuration fields and joins list params', () { final url = const Configuration( url: 'https://app.dev.ramp-network.org/custom', hostApiKey: 'key', hostAppName: 'App', - offrampAsset: 'ETH', + enabledCryptoAssets: ['ETH_*', 'BTC_BTC'], + inAsset: 'EUR', + outAsset: 'ETH_ETH', + inAssetValue: '10000', offrampWebhookV3Url: 'https://example.com/hook', enabledFlows: ['ONRAMP', 'OFFRAMP'], defaultFlow: 'OFFRAMP', @@ -31,15 +34,18 @@ void main() { expect(url.path, '/custom'); expect(url.queryParameters['hostApiKey'], 'key'); expect(url.queryParameters['enabledFlows'], 'ONRAMP,OFFRAMP'); + expect(url.queryParameters['enabledCryptoAssets'], 'ETH_*,BTC_BTC'); + expect(url.queryParameters['inAsset'], 'EUR'); + expect(url.queryParameters['outAsset'], 'ETH_ETH'); + expect(url.queryParameters['inAssetValue'], '10000'); expect(url.queryParameters['useSendCryptoCallbackVersion'], '1'); - expect(url.queryParameters['variant'], 'sdk-mobile'); }); test('omits null and empty optional fields', () { - final url = const Configuration(hostApiKey: '', fiatValue: null).buildWidgetUrl(); + final url = const Configuration(hostApiKey: '', inAssetValue: null).buildWidgetUrl(); expect(url.queryParameters.containsKey('hostApiKey'), isFalse); - expect(url.queryParameters.containsKey('fiatValue'), isFalse); + expect(url.queryParameters.containsKey('inAssetValue'), isFalse); }); }); From 280bc56670d5ee5eba105e099c365c604a4ee8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 18:05:44 +0200 Subject: [PATCH 40/56] add enums for payment methods and flows --- CHANGELOG.md | 10 ++++++---- README.md | 2 +- example/lib/main.dart | 22 +++++++++++----------- lib/src/configuration.dart | 18 ++++++++++++++---- lib/src/flow.dart | 6 ++++++ lib/src/payment_method_type.dart | 13 +++++++++++++ test/ramp_webview_test.dart | 7 +++++-- 7 files changed, 56 insertions(+), 22 deletions(-) create mode 100644 lib/src/flow.dart create mode 100644 lib/src/payment_method_type.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 8401a42..0bd79b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,12 @@ `WidgetCloseRequest`). Host → widget: sealed `HostEvent` (`SendCryptoResult`, `RequestCryptoAccountResult`) via `postHostEvent`. * `Configuration` builds widget URLs with widget-2 params (`inAsset` / - `outAsset` / `inAssetValue` / `outAssetValue` / `enabledCryptoAssets`, plus - `sdkType` / `sdkVersion`). Legacy params (`defaultAsset`, `fiatCurrency`, - `fiatValue`, `swapAsset`, `offrampAsset`, `swapAmount`, `hostLogoUrl`, - `containerNode`, `deepLinkScheme`, `variant`) are removed. + `outAsset` / `inAssetValue` / `outAssetValue` / `enabledCryptoAssets` / + `paymentMethodType`, plus `sdkType` / `sdkVersion`). `defaultFlow` / + `enabledFlows` use `Flow`; `paymentMethodType` uses `PaymentMethodType`. + Legacy params (`defaultAsset`, `fiatCurrency`, `fiatValue`, `swapAsset`, + `offrampAsset`, `swapAmount`, `hostLogoUrl`, `containerNode`, + `deepLinkScheme`, `variant`) are removed. * Default base URL is now `https://app.rampnetwork.com`. * Raise minimum platforms to Android 7.0 (API 24) and iOS 13. * Raise minimum Flutter to 3.44 / Dart 3.12; update `webview_flutter`, diff --git a/README.md b/README.md index 9f61168..18140c4 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ final ramp = RampFlutter( Configuration( hostApiKey: 'YOUR_API_KEY', hostAppName: 'My App', - enabledFlows: ['ONRAMP', 'OFFRAMP'], + enabledFlows: [Flow.ONRAMP, Flow.OFFRAMP], outAsset: 'BTC_BTC', ), ) diff --git a/example/lib/main.dart b/example/lib/main.dart index e6377c3..751a04e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -36,8 +36,8 @@ class _RampFlutterAppState extends State { String? _userAddress; String? _hostAppName = 'Ramp Network Flutter'; String? _hostApiKey; - String? _defaultFlow = 'ONRAMP'; - List _enabledFlows = ['ONRAMP', 'OFFRAMP', 'SWAP']; + Flow? _defaultFlow = Flow.ONRAMP; + List _enabledFlows = [Flow.ONRAMP, Flow.OFFRAMP, Flow.SWAP]; @override void initState() { @@ -70,7 +70,7 @@ class _RampFlutterAppState extends State { url: _predefinedEnvironments[_selectedEnvironment], hostAppName: _hostAppName, defaultFlow: _defaultFlow, - enabledFlows: List.from(_enabledFlows), + enabledFlows: List.from(_enabledFlows), enabledCryptoAssets: enabledCryptoAssets, inAsset: _inAsset, inAssetValue: _inAssetValue, @@ -230,10 +230,10 @@ class _RampFlutterAppState extends State { _textField('Host API key', (text) => _hostApiKey = text, _hostApiKey), _segmentedControl('Default flow:', ['ONRAMP', 'OFFRAMP'], (index) { if (index == 0) { - _defaultFlow = 'ONRAMP'; + _defaultFlow = Flow.ONRAMP; } if (index == 1) { - _defaultFlow = 'OFFRAMP'; + _defaultFlow = Flow.OFFRAMP; } setState(() {}); }), @@ -242,19 +242,19 @@ class _RampFlutterAppState extends State { } Widget _enabledFlowsSection() { - Widget flowSwitch(String name) { + Widget flowSwitch(Flow flow) { return Row( mainAxisSize: MainAxisSize.min, children: [ - Text(name), + Text(flow.name), Switch( - value: _enabledFlows.contains(name), + value: _enabledFlows.contains(flow), onChanged: (enabled) { setState(() { if (enabled) { - _enabledFlows = [..._enabledFlows, name]; + _enabledFlows = [..._enabledFlows, flow]; } else { - _enabledFlows = _enabledFlows.where((flow) => flow != name).toList(); + _enabledFlows = _enabledFlows.where((value) => value != flow).toList(); } }); }, @@ -267,7 +267,7 @@ class _RampFlutterAppState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Enabled flows:'), - Row(children: [flowSwitch('ONRAMP'), flowSwitch('OFFRAMP'), flowSwitch('SWAP')]), + Row(children: [flowSwitch(Flow.ONRAMP), flowSwitch(Flow.OFFRAMP), flowSwitch(Flow.SWAP)]), ], ); } diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index 9af4c60..3360789 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -1,3 +1,9 @@ +import 'package:ramp_flutter/src/flow.dart'; +import 'package:ramp_flutter/src/payment_method_type.dart'; + +export 'package:ramp_flutter/src/flow.dart'; +export 'package:ramp_flutter/src/payment_method_type.dart'; + class Configuration { const Configuration({ this.url, @@ -12,6 +18,7 @@ class Configuration { this.hostApiKey, this.hostAppName, this.offrampWebhookV3Url, + this.paymentMethodType, this.selectedCountryCode, this.userAddress, this.userEmailAddress, @@ -24,8 +31,8 @@ class Configuration { static const String sdkVersion = '5.0.0'; final String? url; - final String? defaultFlow; - final List? enabledFlows; + final Flow? defaultFlow; + final List? enabledFlows; final List? enabledCryptoAssets; final String? inAsset; final String? inAssetValue; @@ -35,6 +42,7 @@ class Configuration { final String? hostApiKey; final String? hostAppName; final String? offrampWebhookV3Url; + final PaymentMethodType? paymentMethodType; final String? selectedCountryCode; final String? userAddress; final String? userEmailAddress; @@ -52,8 +60,9 @@ class Configuration { final queryParameters = { ...base.queryParameters, - if (_nonEmpty(defaultFlow)) 'defaultFlow': defaultFlow!, - if (enabledFlows != null && enabledFlows!.isNotEmpty) 'enabledFlows': enabledFlows!.join(','), + if (defaultFlow != null) 'defaultFlow': defaultFlow!.name, + if (enabledFlows != null && enabledFlows!.isNotEmpty) + 'enabledFlows': enabledFlows!.map((flow) => flow.name).join(','), if (enabledCryptoAssets != null && enabledCryptoAssets!.isNotEmpty) 'enabledCryptoAssets': enabledCryptoAssets!.join(','), if (_nonEmpty(inAsset)) 'inAsset': inAsset!, @@ -64,6 +73,7 @@ class Configuration { if (_nonEmpty(hostApiKey)) 'hostApiKey': hostApiKey!, if (_nonEmpty(hostAppName)) 'hostAppName': hostAppName!, if (_nonEmpty(offrampWebhookV3Url)) 'offrampWebhookV3Url': offrampWebhookV3Url!, + if (paymentMethodType != null) 'paymentMethodType': paymentMethodType!.name, if (_nonEmpty(selectedCountryCode)) 'selectedCountryCode': selectedCountryCode!, if (_nonEmpty(userAddress)) 'userAddress': userAddress!, if (_nonEmpty(userEmailAddress)) 'userEmailAddress': userEmailAddress!, diff --git a/lib/src/flow.dart b/lib/src/flow.dart new file mode 100644 index 0000000..a62a135 --- /dev/null +++ b/lib/src/flow.dart @@ -0,0 +1,6 @@ +/// Widget transaction flow for `defaultFlow` / `enabledFlows`. +enum Flow { + ONRAMP, + OFFRAMP, + SWAP, +} diff --git a/lib/src/payment_method_type.dart b/lib/src/payment_method_type.dart new file mode 100644 index 0000000..b5aa7d7 --- /dev/null +++ b/lib/src/payment_method_type.dart @@ -0,0 +1,13 @@ +/// Host-facing payment method for widget URL `paymentMethodType`. +/// +/// Values match widget-2 `PublicPaymentMethodName` (not internal types like `CARD`). +enum PaymentMethodType { + MANUAL_BANK_TRANSFER, + AUTO_BANK_TRANSFER, + CARD_PAYMENT, + APPLE_PAY, + GOOGLE_PAY, + PIX, + ACH, + PAYPAL, +} diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index 71f1750..30df932 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -25,8 +25,9 @@ void main() { outAsset: 'ETH_ETH', inAssetValue: '10000', offrampWebhookV3Url: 'https://example.com/hook', - enabledFlows: ['ONRAMP', 'OFFRAMP'], - defaultFlow: 'OFFRAMP', + enabledFlows: [Flow.ONRAMP, Flow.OFFRAMP], + defaultFlow: Flow.OFFRAMP, + paymentMethodType: PaymentMethodType.CARD_PAYMENT, useSendCryptoCallback: true, ).buildWidgetUrl(); @@ -38,6 +39,8 @@ void main() { expect(url.queryParameters['inAsset'], 'EUR'); expect(url.queryParameters['outAsset'], 'ETH_ETH'); expect(url.queryParameters['inAssetValue'], '10000'); + expect(url.queryParameters['defaultFlow'], 'OFFRAMP'); + expect(url.queryParameters['paymentMethodType'], 'CARD_PAYMENT'); expect(url.queryParameters['useSendCryptoCallbackVersion'], '1'); }); From d923f1e4f605a29abd699ed5a57c63e0596fbe5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 18:10:32 +0200 Subject: [PATCH 41/56] sanitize widget url --- CHANGELOG.md | 3 ++ lib/src/configuration.dart | 59 ++++++++++++++++---------------- lib/src/flow.dart | 1 - lib/src/payment_method_type.dart | 3 -- lib/src/signed_url.dart | 11 ++++-- test/ramp_webview_test.dart | 11 ++++++ 6 files changed, 51 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bd79b2..3d21fa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ Legacy params (`defaultAsset`, `fiatCurrency`, `fiatValue`, `swapAsset`, `offrampAsset`, `swapAmount`, `hostLogoUrl`, `containerNode`, `deepLinkScheme`, `variant`) are removed. +* `Configuration.buildWidgetUrl()` and `RampFlutter.signed` both require an + `https` Ramp Network host (`*.ramp.network`, `*.rampnetwork.com`, + `*.ramp-network.org`). * Default base URL is now `https://app.rampnetwork.com`. * Raise minimum platforms to Android 7.0 (API 24) and iOS 13. * Raise minimum Flutter to 3.44 / Dart 3.12; update `webview_flutter`, diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index 3360789..b43ecc5 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -1,5 +1,6 @@ import 'package:ramp_flutter/src/flow.dart'; import 'package:ramp_flutter/src/payment_method_type.dart'; +import 'package:ramp_flutter/src/signed_url.dart'; export 'package:ramp_flutter/src/flow.dart'; export 'package:ramp_flutter/src/payment_method_type.dart'; @@ -50,46 +51,44 @@ class Configuration { final String? webhookStatusUrl; Uri buildWidgetUrl() { - final base = Uri.parse((url != null && url!.trim().isNotEmpty) ? url!.trim() : defaultUrl); + final trimmedUrl = url?.trim(); + final base = Uri.parse(_nonEmpty(trimmedUrl) ? trimmedUrl! : defaultUrl); if (base.queryParameters['signature']?.isNotEmpty == true) { throw StateError( 'Configuration.buildWidgetUrl() cannot be used with a signed URL. ' 'Use RampFlutter.signed(...) instead.', ); } - - final queryParameters = { - ...base.queryParameters, - if (defaultFlow != null) 'defaultFlow': defaultFlow!.name, - if (enabledFlows != null && enabledFlows!.isNotEmpty) - 'enabledFlows': enabledFlows!.map((flow) => flow.name).join(','), - if (enabledCryptoAssets != null && enabledCryptoAssets!.isNotEmpty) - 'enabledCryptoAssets': enabledCryptoAssets!.join(','), - if (_nonEmpty(inAsset)) 'inAsset': inAsset!, - if (_nonEmpty(inAssetValue)) 'inAssetValue': inAssetValue!, - if (_nonEmpty(outAsset)) 'outAsset': outAsset!, - if (_nonEmpty(outAssetValue)) 'outAssetValue': outAssetValue!, - if (_nonEmpty(finalUrl)) 'finalUrl': finalUrl!, - if (_nonEmpty(hostApiKey)) 'hostApiKey': hostApiKey!, - if (_nonEmpty(hostAppName)) 'hostAppName': hostAppName!, - if (_nonEmpty(offrampWebhookV3Url)) 'offrampWebhookV3Url': offrampWebhookV3Url!, - if (paymentMethodType != null) 'paymentMethodType': paymentMethodType!.name, - if (_nonEmpty(selectedCountryCode)) 'selectedCountryCode': selectedCountryCode!, - if (_nonEmpty(userAddress)) 'userAddress': userAddress!, - if (_nonEmpty(userEmailAddress)) 'userEmailAddress': userEmailAddress!, - if (_nonEmpty(webhookStatusUrl)) 'webhookStatusUrl': webhookStatusUrl!, - 'sdkType': sdkType, - 'sdkVersion': sdkVersion, - }; - - if (useSendCryptoCallback == true) { - queryParameters['useSendCryptoCallbackVersion'] = '1'; + if (!isTrustedRampWidgetUrl(base)) { + throw ArgumentError.value(base.toString(), 'url', 'Untrusted Ramp Network URL'); } return base.replace( - scheme: base.scheme.isEmpty ? 'https' : base.scheme, path: base.path.isEmpty ? '/' : base.path, - queryParameters: queryParameters, + queryParameters: { + ...base.queryParameters, + if (defaultFlow != null) 'defaultFlow': defaultFlow!.name, + if (enabledFlows != null && enabledFlows!.isNotEmpty) + 'enabledFlows': enabledFlows!.map((flow) => flow.name).join(','), + if (enabledCryptoAssets != null && enabledCryptoAssets!.isNotEmpty) + 'enabledCryptoAssets': enabledCryptoAssets!.join(','), + if (_nonEmpty(inAsset)) 'inAsset': inAsset!, + if (_nonEmpty(inAssetValue)) 'inAssetValue': inAssetValue!, + if (_nonEmpty(outAsset)) 'outAsset': outAsset!, + if (_nonEmpty(outAssetValue)) 'outAssetValue': outAssetValue!, + if (_nonEmpty(finalUrl)) 'finalUrl': finalUrl!, + if (_nonEmpty(hostApiKey)) 'hostApiKey': hostApiKey!, + if (_nonEmpty(hostAppName)) 'hostAppName': hostAppName!, + if (_nonEmpty(offrampWebhookV3Url)) 'offrampWebhookV3Url': offrampWebhookV3Url!, + if (paymentMethodType != null) 'paymentMethodType': paymentMethodType!.name, + if (_nonEmpty(selectedCountryCode)) 'selectedCountryCode': selectedCountryCode!, + if (_nonEmpty(userAddress)) 'userAddress': userAddress!, + if (_nonEmpty(userEmailAddress)) 'userEmailAddress': userEmailAddress!, + if (_nonEmpty(webhookStatusUrl)) 'webhookStatusUrl': webhookStatusUrl!, + if (useSendCryptoCallback == true) 'useSendCryptoCallbackVersion': '1', + 'sdkType': sdkType, + 'sdkVersion': sdkVersion, + }, ); } diff --git a/lib/src/flow.dart b/lib/src/flow.dart index a62a135..7ae568d 100644 --- a/lib/src/flow.dart +++ b/lib/src/flow.dart @@ -1,4 +1,3 @@ -/// Widget transaction flow for `defaultFlow` / `enabledFlows`. enum Flow { ONRAMP, OFFRAMP, diff --git a/lib/src/payment_method_type.dart b/lib/src/payment_method_type.dart index b5aa7d7..471531a 100644 --- a/lib/src/payment_method_type.dart +++ b/lib/src/payment_method_type.dart @@ -1,6 +1,3 @@ -/// Host-facing payment method for widget URL `paymentMethodType`. -/// -/// Values match widget-2 `PublicPaymentMethodName` (not internal types like `CARD`). enum PaymentMethodType { MANUAL_BANK_TRANSFER, AUTO_BANK_TRANSFER, diff --git a/lib/src/signed_url.dart b/lib/src/signed_url.dart index 8548945..4d232bd 100644 --- a/lib/src/signed_url.dart +++ b/lib/src/signed_url.dart @@ -1,12 +1,17 @@ -final _trustedRampHost = RegExp(r'^([a-z0-9-]+\.)*(ramp\.network|rampnetwork\.com|ramp-network\.org)$'); +final _trustedRampHost = RegExp( + r'^([a-z0-9-]+\.)*(ramp\.network|rampnetwork\.com|ramp-network\.org)$', +); + +bool isTrustedRampWidgetUrl(Uri url) { + return url.scheme == 'https' && _trustedRampHost.hasMatch(url.host.toLowerCase()); +} Uri validateRampSignedUrl(String value) { final url = Uri.tryParse(value); final parameters = url?.queryParameters ?? const {}; const requiredParameters = ['hostApiKey', 'timestamp', 'signature']; if (url == null || - url.scheme != 'https' || - !_trustedRampHost.hasMatch(url.host) || + !isTrustedRampWidgetUrl(url) || !requiredParameters.every((parameter) => parameters[parameter]?.isNotEmpty == true)) { throw ArgumentError.value(value, 'url', 'Invalid signed Ramp Network URL'); } diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index 30df932..b91b721 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -50,6 +50,17 @@ void main() { expect(url.queryParameters.containsKey('hostApiKey'), isFalse); expect(url.queryParameters.containsKey('inAssetValue'), isFalse); }); + + test('rejects an untrusted base URL', () { + expect( + () => const Configuration(url: 'https://evil.example/').buildWidgetUrl(), + throwsArgumentError, + ); + expect( + () => const Configuration(url: 'http://app.rampnetwork.com/').buildWidgetUrl(), + throwsArgumentError, + ); + }); }); group('RampFlutter event parsing', () { From efcd8048bbeecb49ede58555928a08a2c778945c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 18:13:25 +0200 Subject: [PATCH 42/56] clean up --- .gitignore | 1 + .../LaunchImage.imageset/README.md | 5 - pubspec.lock | 526 ------------------ ramp_flutter.iml | 18 - 4 files changed, 1 insertion(+), 549 deletions(-) delete mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md delete mode 100644 pubspec.lock delete mode 100644 ramp_flutter.iml diff --git a/.gitignore b/.gitignore index eb6c05c..372f50c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ migrate_working_dir/ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +# The example app lockfile (example/pubspec.lock) is kept for reproducible demos. /pubspec.lock **/doc/api/ .dart_tool/ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725..0000000 --- a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index 17dfa34..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,526 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - args: - dependency: transitive - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: transitive - description: - name: async - sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 - url: "https://pub.dev" - source: hosted - version: "2.13.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.dev" - source: hosted - version: "1.4.1" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" - url: "https://pub.dev" - source: hosted - version: "0.3.5+4" - dbus: - dependency: transitive - description: - name: dbus - sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" - url: "https://pub.dev" - source: hosted - version: "0.7.14" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - ffi: - dependency: transitive - description: - name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - file_picker: - dependency: "direct main" - description: - name: file_picker - sha256: "57d9a1dd5063f85fa3107fb42d1faffda52fdc948cefd5fe5ea85267a5fc7343" - url: "https://pub.dev" - source: hosted - version: "10.3.10" - file_selector_linux: - dependency: transitive - description: - name: file_selector_linux - sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" - url: "https://pub.dev" - source: hosted - version: "0.9.4" - file_selector_macos: - dependency: transitive - description: - name: file_selector_macos - sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" - url: "https://pub.dev" - source: hosted - version: "0.9.5" - file_selector_platform_interface: - dependency: transitive - description: - name: file_selector_platform_interface - sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" - url: "https://pub.dev" - source: hosted - version: "2.7.0" - file_selector_windows: - dependency: transitive - description: - name: file_selector_windows - sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" - url: "https://pub.dev" - source: hosted - version: "0.9.3+5" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" - url: "https://pub.dev" - source: hosted - version: "2.0.35" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - http: - dependency: transitive - description: - name: http - sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" - url: "https://pub.dev" - source: hosted - version: "1.6.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - image_picker: - dependency: "direct main" - description: - name: image_picker - sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667 - url: "https://pub.dev" - source: hosted - version: "1.2.3" - image_picker_android: - dependency: transitive - description: - name: image_picker_android - sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a" - url: "https://pub.dev" - source: hosted - version: "0.8.13+19" - image_picker_for_web: - dependency: transitive - description: - name: image_picker_for_web - sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" - url: "https://pub.dev" - source: hosted - version: "3.1.1" - image_picker_ios: - dependency: transitive - description: - name: image_picker_ios - sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 - url: "https://pub.dev" - source: hosted - version: "0.8.13+6" - image_picker_linux: - dependency: transitive - description: - name: image_picker_linux - sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" - url: "https://pub.dev" - source: hosted - version: "0.2.2" - image_picker_macos: - dependency: transitive - description: - name: image_picker_macos - sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" - url: "https://pub.dev" - source: hosted - version: "0.2.2+1" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" - url: "https://pub.dev" - source: hosted - version: "2.11.1" - image_picker_windows: - dependency: transitive - description: - name: image_picker_windows - sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae - url: "https://pub.dev" - source: hosted - version: "0.2.2" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" - source: hosted - version: "11.0.2" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" - source: hosted - version: "3.0.10" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - lints: - dependency: transitive - description: - name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" - source: hosted - version: "6.1.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 - url: "https://pub.dev" - source: hosted - version: "0.12.19" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.dev" - source: hosted - version: "0.13.0" - meta: - dependency: transitive - description: - name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" - url: "https://pub.dev" - source: hosted - version: "1.18.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.dev" - source: hosted - version: "7.0.2" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" - source: hosted - version: "1.10.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" - url: "https://pub.dev" - source: hosted - version: "0.7.11" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 - url: "https://pub.dev" - source: hosted - version: "6.3.2" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 - url: "https://pub.dev" - source: hosted - version: "6.3.32" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" - url: "https://pub.dev" - source: hosted - version: "6.4.1" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a - url: "https://pub.dev" - source: hosted - version: "3.2.2" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" - url: "https://pub.dev" - source: hosted - version: "3.2.5" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" - url: "https://pub.dev" - source: hosted - version: "2.4.3" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" - url: "https://pub.dev" - source: hosted - version: "3.1.5" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" - source: hosted - version: "2.2.0" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" - url: "https://pub.dev" - source: hosted - version: "15.2.0" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - webview_flutter: - dependency: "direct main" - description: - path: "packages/webview_flutter/webview_flutter" - ref: a37d157a9160fbd509484e9bb176be038c4d153c - resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c - url: "https://github.com/mateusz-ramp/flutter_packages.git" - source: git - version: "4.14.1" - webview_flutter_android: - dependency: "direct main" - description: - path: "packages/webview_flutter/webview_flutter_android" - ref: a37d157a9160fbd509484e9bb176be038c4d153c - resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c - url: "https://github.com/mateusz-ramp/flutter_packages.git" - source: git - version: "4.13.0" - webview_flutter_platform_interface: - dependency: transitive - description: - path: "packages/webview_flutter/webview_flutter_platform_interface" - ref: a37d157a9160fbd509484e9bb176be038c4d153c - resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c - url: "https://github.com/mateusz-ramp/flutter_packages.git" - source: git - version: "2.15.1" - webview_flutter_wkwebview: - dependency: "direct main" - description: - path: "packages/webview_flutter/webview_flutter_wkwebview" - ref: a37d157a9160fbd509484e9bb176be038c4d153c - resolved-ref: a37d157a9160fbd509484e9bb176be038c4d153c - url: "https://github.com/mateusz-ramp/flutter_packages.git" - source: git - version: "3.26.0" - win32: - dependency: transitive - description: - name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e - url: "https://pub.dev" - source: hosted - version: "5.15.0" - xml: - dependency: transitive - description: - name: xml - sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.dev" - source: hosted - version: "6.6.1" -sdks: - dart: ">=3.12.2 <4.0.0" - flutter: ">=3.44.0" diff --git a/ramp_flutter.iml b/ramp_flutter.iml deleted file mode 100644 index 118dd2c..0000000 --- a/ramp_flutter.iml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - From bcb3eefe1bb61ae757738280412846fd7a19d28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 18:27:56 +0200 Subject: [PATCH 43/56] add makefile, fix analyze issues --- CHANGELOG.md | 3 +- Makefile | 19 ++++++ README.md | 2 +- example/ios/Runner.xcodeproj/project.pbxproj | 64 ++++++++++++++++++- example/lib/main.dart | 20 ++++-- lib/src/configuration.dart | 10 ++- .../{payment_method_type.dart => enums.dart} | 4 ++ lib/src/flow.dart | 5 -- lib/src/signed_url.dart | 4 +- test/ramp_webview_test.dart | 14 ++-- 10 files changed, 111 insertions(+), 34 deletions(-) create mode 100644 Makefile rename lib/src/{payment_method_type.dart => enums.dart} (59%) delete mode 100644 lib/src/flow.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d21fa8..38be06d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ * `Configuration` builds widget URLs with widget-2 params (`inAsset` / `outAsset` / `inAssetValue` / `outAssetValue` / `enabledCryptoAssets` / `paymentMethodType`, plus `sdkType` / `sdkVersion`). `defaultFlow` / - `enabledFlows` use `Flow`; `paymentMethodType` uses `PaymentMethodType`. + `enabledFlows` use `TransactionFlow`; `paymentMethodType` uses + `PaymentMethodType`. Legacy params (`defaultAsset`, `fiatCurrency`, `fiatValue`, `swapAsset`, `offrampAsset`, `swapAmount`, `hostLogoUrl`, `containerNode`, `deepLinkScheme`, `variant`) are removed. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3ae877c --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +.PHONY: get analyze format test ios android + +get: + flutter pub get + +analyze: + dart analyze + +format: + dart format --line-length 120 lib test example/lib + +test: + flutter test + +ios: + cd example && flutter build ios --debug --no-codesign + +android: + cd example && flutter build apk --debug diff --git a/README.md b/README.md index 18140c4..d8a159a 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ final ramp = RampFlutter( Configuration( hostApiKey: 'YOUR_API_KEY', hostAppName: 'My App', - enabledFlows: [Flow.ONRAMP, Flow.OFFRAMP], + enabledFlows: [TransactionFlow.ONRAMP, TransactionFlow.OFFRAMP], outAsset: 'BTC_BTC', ), ) diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index e87f5fe..3a7fedd 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,6 +8,16 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -26,6 +36,30 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -110,7 +144,32 @@ LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { - /* End PBXProject section */ + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 97C146EC1CF9000F007C117D /* Resources */ = { @@ -173,6 +232,9 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; diff --git a/example/lib/main.dart b/example/lib/main.dart index 751a04e..0244063 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -36,8 +36,8 @@ class _RampFlutterAppState extends State { String? _userAddress; String? _hostAppName = 'Ramp Network Flutter'; String? _hostApiKey; - Flow? _defaultFlow = Flow.ONRAMP; - List _enabledFlows = [Flow.ONRAMP, Flow.OFFRAMP, Flow.SWAP]; + TransactionFlow? _defaultFlow = TransactionFlow.ONRAMP; + List _enabledFlows = [TransactionFlow.ONRAMP, TransactionFlow.OFFRAMP, TransactionFlow.SWAP]; @override void initState() { @@ -70,7 +70,7 @@ class _RampFlutterAppState extends State { url: _predefinedEnvironments[_selectedEnvironment], hostAppName: _hostAppName, defaultFlow: _defaultFlow, - enabledFlows: List.from(_enabledFlows), + enabledFlows: List.from(_enabledFlows), enabledCryptoAssets: enabledCryptoAssets, inAsset: _inAsset, inAssetValue: _inAssetValue, @@ -230,10 +230,10 @@ class _RampFlutterAppState extends State { _textField('Host API key', (text) => _hostApiKey = text, _hostApiKey), _segmentedControl('Default flow:', ['ONRAMP', 'OFFRAMP'], (index) { if (index == 0) { - _defaultFlow = Flow.ONRAMP; + _defaultFlow = TransactionFlow.ONRAMP; } if (index == 1) { - _defaultFlow = Flow.OFFRAMP; + _defaultFlow = TransactionFlow.OFFRAMP; } setState(() {}); }), @@ -242,7 +242,7 @@ class _RampFlutterAppState extends State { } Widget _enabledFlowsSection() { - Widget flowSwitch(Flow flow) { + Widget flowSwitch(TransactionFlow flow) { return Row( mainAxisSize: MainAxisSize.min, children: [ @@ -267,7 +267,13 @@ class _RampFlutterAppState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Enabled flows:'), - Row(children: [flowSwitch(Flow.ONRAMP), flowSwitch(Flow.OFFRAMP), flowSwitch(Flow.SWAP)]), + Row( + children: [ + flowSwitch(TransactionFlow.ONRAMP), + flowSwitch(TransactionFlow.OFFRAMP), + flowSwitch(TransactionFlow.SWAP), + ], + ), ], ); } diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index b43ecc5..f179df9 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -1,9 +1,7 @@ -import 'package:ramp_flutter/src/flow.dart'; -import 'package:ramp_flutter/src/payment_method_type.dart'; +import 'package:ramp_flutter/src/enums.dart'; import 'package:ramp_flutter/src/signed_url.dart'; -export 'package:ramp_flutter/src/flow.dart'; -export 'package:ramp_flutter/src/payment_method_type.dart'; +export 'package:ramp_flutter/src/enums.dart'; class Configuration { const Configuration({ @@ -32,8 +30,8 @@ class Configuration { static const String sdkVersion = '5.0.0'; final String? url; - final Flow? defaultFlow; - final List? enabledFlows; + final TransactionFlow? defaultFlow; + final List? enabledFlows; final List? enabledCryptoAssets; final String? inAsset; final String? inAssetValue; diff --git a/lib/src/payment_method_type.dart b/lib/src/enums.dart similarity index 59% rename from lib/src/payment_method_type.dart rename to lib/src/enums.dart index 471531a..ab37b73 100644 --- a/lib/src/payment_method_type.dart +++ b/lib/src/enums.dart @@ -1,3 +1,7 @@ +// ignore_for_file: constant_identifier_names + +enum TransactionFlow { ONRAMP, OFFRAMP, SWAP } + enum PaymentMethodType { MANUAL_BANK_TRANSFER, AUTO_BANK_TRANSFER, diff --git a/lib/src/flow.dart b/lib/src/flow.dart deleted file mode 100644 index 7ae568d..0000000 --- a/lib/src/flow.dart +++ /dev/null @@ -1,5 +0,0 @@ -enum Flow { - ONRAMP, - OFFRAMP, - SWAP, -} diff --git a/lib/src/signed_url.dart b/lib/src/signed_url.dart index 4d232bd..fea808c 100644 --- a/lib/src/signed_url.dart +++ b/lib/src/signed_url.dart @@ -1,6 +1,4 @@ -final _trustedRampHost = RegExp( - r'^([a-z0-9-]+\.)*(ramp\.network|rampnetwork\.com|ramp-network\.org)$', -); +final _trustedRampHost = RegExp(r'^([a-z0-9-]+\.)*(ramp\.network|rampnetwork\.com|ramp-network\.org)$'); bool isTrustedRampWidgetUrl(Uri url) { return url.scheme == 'https' && _trustedRampHost.hasMatch(url.host.toLowerCase()); diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index b91b721..8b7b14c 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -25,8 +25,8 @@ void main() { outAsset: 'ETH_ETH', inAssetValue: '10000', offrampWebhookV3Url: 'https://example.com/hook', - enabledFlows: [Flow.ONRAMP, Flow.OFFRAMP], - defaultFlow: Flow.OFFRAMP, + enabledFlows: [TransactionFlow.ONRAMP, TransactionFlow.OFFRAMP], + defaultFlow: TransactionFlow.OFFRAMP, paymentMethodType: PaymentMethodType.CARD_PAYMENT, useSendCryptoCallback: true, ).buildWidgetUrl(); @@ -52,14 +52,8 @@ void main() { }); test('rejects an untrusted base URL', () { - expect( - () => const Configuration(url: 'https://evil.example/').buildWidgetUrl(), - throwsArgumentError, - ); - expect( - () => const Configuration(url: 'http://app.rampnetwork.com/').buildWidgetUrl(), - throwsArgumentError, - ); + expect(() => const Configuration(url: 'https://evil.example/').buildWidgetUrl(), throwsArgumentError); + expect(() => const Configuration(url: 'http://app.rampnetwork.com/').buildWidgetUrl(), throwsArgumentError); }); }); From 44bfb09d1f189b73b9b33d8368ccbb69b02c1745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 18:35:21 +0200 Subject: [PATCH 44/56] add more tests --- test/android_file_selector_test.dart | 37 ++++++++++++++++++ test/external_navigation_test.dart | 58 ++++++++++++++++++++++++++++ test/ramp_webview_test.dart | 26 ++++++++++++- 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 test/android_file_selector_test.dart create mode 100644 test/external_navigation_test.dart diff --git a/test/android_file_selector_test.dart b/test/android_file_selector_test.dart new file mode 100644 index 0000000..70e3594 --- /dev/null +++ b/test/android_file_selector_test.dart @@ -0,0 +1,37 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:ramp_flutter/src/android_file_selector.dart'; + +void main() { + group('fileTypeForAcceptTypes', () { + test('defaults to any when empty', () { + expect(fileTypeForAcceptTypes(const []), FileType.any); + }); + + test('maps image and video mime families', () { + expect(fileTypeForAcceptTypes(const ['image/png', 'image/jpeg']), FileType.image); + expect(fileTypeForAcceptTypes(const ['video/mp4']), FileType.video); + }); + + test('maps extension and pdf accepts to custom', () { + expect(fileTypeForAcceptTypes(const ['.pdf', 'application/pdf']), FileType.custom); + expect(fileTypeForAcceptTypes(const ['.png', '.jpg']), FileType.custom); + }); + + test('falls back to any for mixed or unknown accepts', () { + expect(fileTypeForAcceptTypes(const ['image/png', 'application/pdf']), FileType.any); + expect(fileTypeForAcceptTypes(const ['application/json']), FileType.any); + }); + }); + + group('extensionsForAcceptTypes', () { + test('extracts dotted extensions and pdf', () { + expect(extensionsForAcceptTypes(const ['.PNG', 'application/pdf', ' .jpg ']), ['png', 'pdf', 'jpg']); + }); + + test('returns null when nothing maps to an extension', () { + expect(extensionsForAcceptTypes(const ['image/png', 'video/mp4']), isNull); + expect(extensionsForAcceptTypes(const []), isNull); + }); + }); +} diff --git a/test/external_navigation_test.dart b/test/external_navigation_test.dart new file mode 100644 index 0000000..23cc054 --- /dev/null +++ b/test/external_navigation_test.dart @@ -0,0 +1,58 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:ramp_flutter/src/external_navigation.dart'; + +void main() { + final widgetUrl = Uri.parse('https://app.rampnetwork.com/'); + + group('shouldOpenExternally', () { + test('keeps about and empty schemes in the WebView', () { + expect(shouldOpenExternally(Uri.parse('about:blank'), widgetUrl), isFalse); + expect(shouldOpenExternally(Uri.parse('/relative'), widgetUrl), isFalse); + }); + + test('keeps same-host https navigations in the WebView', () { + expect(shouldOpenExternally(Uri.parse('https://app.rampnetwork.com/checkout'), widgetUrl), isFalse); + expect(shouldOpenExternally(Uri.parse('https://APP.RAMPNETWORK.COM/path'), widgetUrl), isFalse); + }); + + test('opens other https hosts externally unless allowlisted', () { + expect(shouldOpenExternally(Uri.parse('https://example.com/pay'), widgetUrl), isTrue); + }); + + test('opens non-http schemes externally', () { + expect(shouldOpenExternally(Uri.parse('mailto:test@example.com'), widgetUrl), isTrue); + expect(shouldOpenExternally(Uri.parse('ramp://return'), widgetUrl), isTrue); + expect(shouldOpenExternally(Uri.parse('intent://pay#Intent;scheme=https;end'), widgetUrl), isTrue); + }); + }); + + group('staysInWebView', () { + test('allows reCAPTCHA hosts', () { + expect(staysInWebView(Uri.parse('https://www.recaptcha.net/recaptcha')), isTrue); + expect(staysInWebView(Uri.parse('https://www.google.com/recaptcha/api.js')), isTrue); + expect(staysInWebView(Uri.parse('https://www.gstatic.com/recaptcha/releases/x')), isTrue); + }); + + test('rejects unrelated google hosts and paths', () { + expect(staysInWebView(Uri.parse('https://www.google.com/maps')), isFalse); + expect(staysInWebView(Uri.parse('https://accounts.google.com/')), isFalse); + expect(staysInWebView(Uri.parse('https://example.com/')), isFalse); + }); + }); + + group('intentFallbackUrl', () { + test('parses browser_fallback_url query param', () { + expect( + intentFallbackUrl( + Uri.parse('intent://pay/?browser_fallback_url=https%3A%2F%2Fexample.com%2Ffallback#Intent;end'), + ), + Uri.parse('https://example.com/fallback'), + ); + }); + + test('returns null when fallback is missing or empty', () { + expect(intentFallbackUrl(Uri.parse('intent://pay#Intent;scheme=https;end')), isNull); + expect(intentFallbackUrl(Uri.parse('intent://pay/?browser_fallback_url=#Intent;end')), isNull); + }); + }); +} diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index 8b7b14c..975ce98 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -24,7 +24,13 @@ void main() { inAsset: 'EUR', outAsset: 'ETH_ETH', inAssetValue: '10000', + outAssetValue: '500000', + finalUrl: 'https://example.com/done', + webhookStatusUrl: 'https://example.com/purchase-hook', offrampWebhookV3Url: 'https://example.com/hook', + selectedCountryCode: 'PL', + userAddress: '0xabc', + userEmailAddress: 'user@example.com', enabledFlows: [TransactionFlow.ONRAMP, TransactionFlow.OFFRAMP], defaultFlow: TransactionFlow.OFFRAMP, paymentMethodType: PaymentMethodType.CARD_PAYMENT, @@ -34,21 +40,39 @@ void main() { expect(url.host, 'app.dev.ramp-network.org'); expect(url.path, '/custom'); expect(url.queryParameters['hostApiKey'], 'key'); + expect(url.queryParameters['hostAppName'], 'App'); expect(url.queryParameters['enabledFlows'], 'ONRAMP,OFFRAMP'); expect(url.queryParameters['enabledCryptoAssets'], 'ETH_*,BTC_BTC'); expect(url.queryParameters['inAsset'], 'EUR'); expect(url.queryParameters['outAsset'], 'ETH_ETH'); expect(url.queryParameters['inAssetValue'], '10000'); + expect(url.queryParameters['outAssetValue'], '500000'); + expect(url.queryParameters['finalUrl'], 'https://example.com/done'); + expect(url.queryParameters['webhookStatusUrl'], 'https://example.com/purchase-hook'); + expect(url.queryParameters['offrampWebhookV3Url'], 'https://example.com/hook'); + expect(url.queryParameters['selectedCountryCode'], 'PL'); + expect(url.queryParameters['userAddress'], '0xabc'); + expect(url.queryParameters['userEmailAddress'], 'user@example.com'); expect(url.queryParameters['defaultFlow'], 'OFFRAMP'); expect(url.queryParameters['paymentMethodType'], 'CARD_PAYMENT'); expect(url.queryParameters['useSendCryptoCallbackVersion'], '1'); }); test('omits null and empty optional fields', () { - final url = const Configuration(hostApiKey: '', inAssetValue: null).buildWidgetUrl(); + final url = const Configuration( + hostApiKey: '', + inAssetValue: null, + enabledFlows: [], + enabledCryptoAssets: [], + url: ' ', + ).buildWidgetUrl(); + expect(url.host, 'app.rampnetwork.com'); expect(url.queryParameters.containsKey('hostApiKey'), isFalse); expect(url.queryParameters.containsKey('inAssetValue'), isFalse); + expect(url.queryParameters.containsKey('enabledFlows'), isFalse); + expect(url.queryParameters.containsKey('enabledCryptoAssets'), isFalse); + expect(url.queryParameters.containsKey('useSendCryptoCallbackVersion'), isFalse); }); test('rejects an untrusted base URL', () { From 4af1cd651341b5577268dd880d7a8cac4f0575ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Wed, 29 Jul 2026 18:37:25 +0200 Subject: [PATCH 45/56] restore changelog --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38be06d..bedd9af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,3 +50,28 @@ ## 3.0.0 * Regenerated plugin to fix platform specific dependencies + +## 2.0.2 + +* Update the default URL to the app + +## 2.0.1 + +* Fix missing `onPurchaseFailed` callback + +## 2.0.0 + +* Update native Ramp Network sdk to support off-ramp + +## 1.0.2 + +* Fixed Purchase object decoding for Android +* Updated URL in README + +## 1.0.1 + +* Fix `An operation is not implemented: Not yet implemented` exception + +## 1.0.0 + +* Initial release. From a3d13be1aebbef71b8347803b3f34a4c31cdf4d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Mon, 3 Aug 2026 11:53:08 +0200 Subject: [PATCH 46/56] add migration guide --- CHANGELOG.md | 38 +++--------------- MIGRATION.md | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 25 +++++------- 3 files changed, 128 insertions(+), 46 deletions(-) create mode 100644 MIGRATION.md diff --git a/CHANGELOG.md b/CHANGELOG.md index bedd9af..38408b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,39 +5,13 @@ * Rewrite the SDK to load the Ramp widget in a Flutter WebView instead of the native iOS/Android Ramp SDKs. Published as a Dart Flutter package (no Android/iOS plugin shells). -* Breaking: SDK no longer presents UI. Create `RampFlutter(Configuration)` or - `RampFlutter.signed(url)`, embed `ramp.view` in your own route/sheet, and - call `dispose` when done. `Configuration` is an immutable helper used only - for the config-built entry point. -* Widget → host: sealed `WidgetEvent` via `onWidgetEvent` (includes - `WidgetCloseRequest`). Host → widget: sealed `HostEvent` - (`SendCryptoResult`, `RequestCryptoAccountResult`) via `postHostEvent`. -* `Configuration` builds widget URLs with widget-2 params (`inAsset` / - `outAsset` / `inAssetValue` / `outAssetValue` / `enabledCryptoAssets` / - `paymentMethodType`, plus `sdkType` / `sdkVersion`). `defaultFlow` / - `enabledFlows` use `TransactionFlow`; `paymentMethodType` uses - `PaymentMethodType`. - Legacy params (`defaultAsset`, `fiatCurrency`, `fiatValue`, `swapAsset`, - `offrampAsset`, `swapAmount`, `hostLogoUrl`, `containerNode`, - `deepLinkScheme`, `variant`) are removed. -* `Configuration.buildWidgetUrl()` and `RampFlutter.signed` both require an - `https` Ramp Network host (`*.ramp.network`, `*.rampnetwork.com`, - `*.ramp-network.org`). +* Breaking: host owns presentation (`ramp.view`); use sealed `WidgetEvent` / + `HostEvent` instead of the old callbacks; `Configuration` is immutable and + uses widget-2 params. See [MIGRATION.md](MIGRATION.md). * Default base URL is now `https://app.rampnetwork.com`. -* Raise minimum platforms to Android 7.0 (API 24) and iOS 13. -* Raise minimum Flutter to 3.44 / Dart 3.12; update `webview_flutter`, - `url_launcher`, and `flutter_lints`. -* Open off-widget navigations and `target=_blank` / `window.open` in the system - browser (same-host stays in the WebView). Depends on a forked - `webview_flutter` (`flutter_packages` / `webview-target-blank`) as a direct - git dependency so hosts pick it up transitively. -* Android WebView file inputs use `file_picker`; capture requests use - `image_picker` (camera). -* Queue `postHostEvent` until the widget page has finished loading. -* Public API is `package:ramp_flutter/ramp_flutter.dart` only (implementation under `lib/src/`). -* Add `RampFlutter.signed` for server-signed widget URLs (loaded verbatim). - Config URL building rejects bases that already include a `signature` query - parameter. +* Raise minimums: Flutter 3.44 / Dart 3.12, Android API 24, iOS 13. +* Off-widget / `target=_blank` navigations open in the system browser; Android + file inputs use `file_picker` / `image_picker`. ## 4.0.1 diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..c8d0497 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,111 @@ +# Migrating from 4.x to 5.x + +Version 5.0.0 replaces the native iOS/Android Ramp SDKs with a Flutter WebView +that loads the Ramp widget. The package is a Dart Flutter package (no Android +or iOS plugin shells). + +## Platform requirements + +| | 4.x | 5.x | +| --- | --- | --- | +| Flutter / Dart | Flutter `>=3.3.0` / Dart `>=3.2.3 <4.0.0` | Flutter 3.44+ / Dart 3.12+ | +| Android | `minSdkVersion` 21 | `minSdkVersion` 24+ | +| iOS | deployment target 11.0 | deployment target 13+ | + +Declare `android.permission.CAMERA`, `NSCameraUsageDescription`, and photo +library usage descriptions for identity verification and document upload. + +## Presentation + +**4.x:** the SDK presented its own UI (`showRamp` / similar). + +**5.x:** create `RampFlutter(Configuration)` or `RampFlutter.signed(url)`, +embed `ramp.view` in your own route / sheet / dialog, and call `dispose` when +dismissed. + +```dart +final ramp = RampFlutter(Configuration(/* ... */)) + ..onWidgetEvent = (event) { /* ... */ }; + +await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => SizedBox( + height: MediaQuery.sizeOf(context).height * 0.92, + child: ramp.view, + ), +); + +ramp.dispose(); +``` + +## Events + +**4.x:** separate callbacks (`onOnrampPurchaseCreated`, `onSendCryptoRequested`, +`onOfframpSaleCreated`, `onRampClosed`, …). + +**5.x:** one `onWidgetEvent` handler with sealed `WidgetEvent` subtypes, and +`postHostEvent` with sealed `HostEvent` subtypes (`SendCryptoResult`, +`RequestCryptoAccountResult`). Host events are queued until the widget page +has finished loading. + +```dart +ramp.onWidgetEvent = (event) { + switch (event) { + case PurchaseCreated(:final payload): + break; + case OfframpSaleCreated(:final payload): + break; + case SendCryptoRequested(:final payload): + ramp.postHostEvent(SendCryptoResult.txHash(txHash)); + case RequestCryptoAccount(:final payload): + ramp.postHostEvent( + RequestCryptoAccountResult.account(address: userAddress, type: payload.type), + ); + case WidgetClose(): + Navigator.of(context).pop(); + case WidgetCloseRequest(): + break; + case WidgetConfigDone(): + case WidgetConfigFailed(): + break; + } +}; +``` + +## Configuration + +`Configuration` is an immutable helper used only for the config-built entry +point. It builds widget URLs with widget-2 params and always appends +`sdkType` / `sdkVersion`. + +### Param mapping + +| 4.x (removed) | 5.x | +| --- | --- | +| `defaultAsset` / `fiatCurrency` / `fiatValue` / `swapAsset` / `offrampAsset` / `swapAmount` | `inAsset` / `outAsset` / `inAssetValue` / `outAssetValue` / `enabledCryptoAssets` | +| `enabledFlows` / `defaultFlow` as strings | `TransactionFlow` enum | +| — | `paymentMethodType` (`PaymentMethodType` enum) | +| `hostLogoUrl`, `containerNode`, `deepLinkScheme`, `variant` | removed | + +Default base URL is `https://app.rampnetwork.com`. + +Both `Configuration.buildWidgetUrl()` and `RampFlutter.signed` require an +`https` Ramp Network host (`*.ramp.network`, `*.rampnetwork.com`, +`*.ramp-network.org`). + +## Signed URLs + +Use `RampFlutter.signed(url)` for server-signed widget URLs (loaded verbatim). +Do not put a signed URL in `Configuration.url` — `buildWidgetUrl()` rejects +bases that already include a `signature` query parameter. + +## Public API surface + +Import only `package:ramp_flutter/ramp_flutter.dart`. Implementation lives under +`lib/src/`. + +## Further reading + +See [README.md](README.md) for host setup and usage examples, and the +[Flutter SDK docs](https://docs.ramp.network/mobile/flutter-sdk/). diff --git a/README.md b/README.md index d8a159a..e8926a3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Ramp Network Flutter -Official Flutter package for Ramp Network. Loads the Ramp widget in a Flutter -WebView on iOS and Android (no native Ramp SDK or Flutter plugin shells). +Official Flutter package for Ramp Network. \ +Loads the Ramp widget in a Flutter +WebView on iOS and Android. ## Getting Started @@ -12,15 +13,14 @@ dependencies: ramp_flutter: ^5.0.0 ``` +Upgrading from 4.x? See [MIGRATION.md](MIGRATION.md). + ### Host setup -- **Android:** `minSdkVersion` 24+. Declare `android.permission.CAMERA` if you - need KYC camera capture. -- **iOS:** deployment target 13+. Add `NSCameraUsageDescription` (and photo - library usage if you rely on document upload). -- Native Ramp iOS/Android SDKs and empty Flutter plugin shells are **not** - required (no CocoaPods `Ramp` pod, no JitPack `ramp-sdk-android`). - Platform WebView support comes from `webview_flutter` / `url_launcher`. +- **Android:** `minSdkVersion` 24+. Declare `android.permission.CAMERA` for + identity verification camera capture. +- **iOS:** deployment target 13+. Add `NSCameraUsageDescription` and photo + library usage descriptions for identity verification and document upload. - Requires Flutter 3.44+ / Dart 3.12+. ### Usage @@ -44,7 +44,6 @@ final ramp = RampFlutter( ..onWidgetEvent = (event) { switch (event) { case PurchaseCreated(:final payload): - // payload.purchase, purchaseViewToken, apiUrl break; case OfframpSaleCreated(:final payload): break; @@ -57,7 +56,6 @@ final ramp = RampFlutter( case WidgetClose(): Navigator.of(context).pop(); case WidgetCloseRequest(): - // User tried to dismiss while widget is not closeable — confirm or ignore. break; case WidgetConfigDone(): case WidgetConfigFailed(): @@ -100,6 +98,5 @@ For more configuration parameters see pick it up transitively (no host `dependency_overrides` required unless another package pins pub.dev `webview_flutter`). - Android WebView `` uses `file_picker` (and the camera via - `image_picker` when capture is requested). Host apps need camera / photo - library usage descriptions (see Getting Started). iOS WKWebView handles file - inputs natively. + `image_picker` for capture). Host apps must declare camera / photo library + usage (see Getting Started). iOS WKWebView handles file inputs natively. From 78e2f1b7a4e9febabaca356d90172dd511d4acd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Mon, 3 Aug 2026 20:08:32 +0200 Subject: [PATCH 47/56] add make command for running ios sim --- Makefile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 3ae877c..e789c3b 100644 --- a/Makefile +++ b/Makefile @@ -12,8 +12,10 @@ format: test: flutter test -ios: +build: cd example && flutter build ios --debug --no-codesign - -android: cd example && flutter build apk --debug + +ios: + flutter emulators --launch apple_ios_simulator + cd example && flutter run -d "iPhone" From 4e1aa13d50029081fe336173e009e3d1caa49209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Mon, 3 Aug 2026 20:13:57 +0200 Subject: [PATCH 48/56] add separate widget for signed url in example app --- example/lib/configuration_form.dart | 183 ++++++++++++++ example/lib/main.dart | 375 +++++----------------------- example/lib/signed_url_form.dart | 86 +++++++ 3 files changed, 337 insertions(+), 307 deletions(-) create mode 100644 example/lib/configuration_form.dart create mode 100644 example/lib/signed_url_form.dart diff --git a/example/lib/configuration_form.dart b/example/lib/configuration_form.dart new file mode 100644 index 0000000..19ce7c5 --- /dev/null +++ b/example/lib/configuration_form.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; + +import 'package:ramp_flutter/ramp_flutter.dart'; + +class ConfigurationForm extends StatefulWidget { + const ConfigurationForm({super.key, this.initialConfiguration}); + + final Configuration? initialConfiguration; + + static Configuration defaults() => const Configuration( + url: 'https://app.dev.ramp-network.org', + hostAppName: 'Ramp Network Flutter', + enabledFlows: [TransactionFlow.ONRAMP, TransactionFlow.OFFRAMP, TransactionFlow.SWAP], + defaultFlow: TransactionFlow.ONRAMP, + outAsset: 'BTC_BTC', + useSendCryptoCallback: true, + ); + + @override + State createState() => ConfigurationFormState(); +} + +class ConfigurationFormState extends State { + static const _environments = [ + 'https://app.dev.ramp-network.org', + 'https://app.demo.ramp.network', + 'https://app.rampnetwork.com', + ]; + + late final TextEditingController _hostApiKey; + late final TextEditingController _hostAppName; + late final TextEditingController _inAsset; + late final TextEditingController _inAssetValue; + late final TextEditingController _outAsset; + late final TextEditingController _outAssetValue; + late final TextEditingController _userEmailAddress; + late final TextEditingController _userAddress; + late String _url; + late TransactionFlow _defaultFlow; + late List _enabledFlows; + + Configuration get configuration => _build(); + + @override + void initState() { + super.initState(); + final initial = widget.initialConfiguration ?? ConfigurationForm.defaults(); + _url = initial.url ?? _environments.first; + _hostApiKey = TextEditingController(text: initial.hostApiKey ?? ''); + _hostAppName = TextEditingController(text: initial.hostAppName ?? ''); + _inAsset = TextEditingController(text: initial.inAsset ?? ''); + _inAssetValue = TextEditingController(text: initial.inAssetValue ?? ''); + _outAsset = TextEditingController(text: initial.outAsset ?? ''); + _outAssetValue = TextEditingController(text: initial.outAssetValue ?? ''); + _userEmailAddress = TextEditingController(text: initial.userEmailAddress ?? ''); + _userAddress = TextEditingController(text: initial.userAddress ?? ''); + _defaultFlow = initial.defaultFlow ?? TransactionFlow.ONRAMP; + _enabledFlows = List.from( + initial.enabledFlows ?? + const [TransactionFlow.ONRAMP, TransactionFlow.OFFRAMP, TransactionFlow.SWAP], + ); + } + + List get _textControllers => [ + _hostApiKey, + _hostAppName, + _inAsset, + _inAssetValue, + _outAsset, + _outAssetValue, + _userEmailAddress, + _userAddress, + ]; + + @override + void dispose() { + for (final controller in _textControllers) { + controller.dispose(); + } + super.dispose(); + } + + String? _trimmed(TextEditingController controller) { + final value = controller.text.trim(); + return value.isEmpty ? null : value; + } + + Configuration _build() => Configuration( + url: _url, + hostApiKey: _trimmed(_hostApiKey), + hostAppName: _trimmed(_hostAppName), + defaultFlow: _defaultFlow, + enabledFlows: List.from(_enabledFlows), + inAsset: _trimmed(_inAsset), + inAssetValue: _trimmed(_inAssetValue), + outAsset: _trimmed(_outAsset), + outAssetValue: _trimmed(_outAssetValue), + userEmailAddress: _trimmed(_userEmailAddress), + userAddress: _trimmed(_userAddress), + useSendCryptoCallback: true, + ); + + Widget _field(TextEditingController controller, String label) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: TextField( + controller: controller, + decoration: InputDecoration(labelText: label, border: const OutlineInputBorder()), + ), + ); + } + + @override + Widget build(BuildContext context) { + return ListView( + children: [ + const Text('Environment'), + SegmentedButton( + showSelectedIcon: false, + expandedInsets: EdgeInsets.zero, + segments: const [ + ButtonSegment(value: 'https://app.dev.ramp-network.org', label: Text('dev')), + ButtonSegment(value: 'https://app.demo.ramp.network', label: Text('demo')), + ButtonSegment(value: 'https://app.rampnetwork.com', label: Text('prod')), + ], + selected: {_url}, + onSelectionChanged: (selection) => setState(() => _url = selection.single), + ), + const SizedBox(height: 12), + _field(_hostApiKey, 'Host API key'), + _field(_hostAppName, 'Host app name'), + const Text('Enabled flows'), + Row( + children: [ + for (final flow in TransactionFlow.values) + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: FilterChip( + label: SizedBox( + width: double.infinity, + child: Text(flow.name, textAlign: TextAlign.center), + ), + showCheckmark: false, + selected: _enabledFlows.contains(flow), + onSelected: (selected) { + setState(() { + if (selected) { + _enabledFlows = [..._enabledFlows, flow]; + } else { + _enabledFlows = _enabledFlows.where((value) => value != flow).toList(); + } + }); + }, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + const Text('Default flow'), + SegmentedButton( + showSelectedIcon: false, + expandedInsets: EdgeInsets.zero, + segments: const [ + ButtonSegment(value: TransactionFlow.ONRAMP, label: Text('ONRAMP')), + ButtonSegment(value: TransactionFlow.OFFRAMP, label: Text('OFFRAMP')), + ButtonSegment(value: TransactionFlow.SWAP, label: Text('SWAP')), + ], + selected: {_defaultFlow}, + onSelectionChanged: (selection) => setState(() => _defaultFlow = selection.single), + ), + const SizedBox(height: 12), + _field(_inAsset, 'In asset'), + _field(_inAssetValue, 'In asset value'), + _field(_outAsset, 'Out asset'), + _field(_outAssetValue, 'Out asset value'), + _field(_userEmailAddress, 'User email address'), + _field(_userAddress, 'Wallet address'), + ], + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 0244063..21f556a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,14 +1,17 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:ramp_flutter/ramp_flutter.dart'; +import 'configuration_form.dart'; +import 'signed_url_form.dart'; + void main() { WidgetsFlutterBinding.ensureInitialized(); runApp(const RampFlutterApp()); } +enum LaunchMode { configuration, signedUrl } + class RampFlutterApp extends StatefulWidget { const RampFlutterApp({super.key}); @@ -17,159 +20,47 @@ class RampFlutterApp extends StatefulWidget { } class _RampFlutterAppState extends State { - final ValueNotifier> _debugEvents = ValueNotifier>(const []); - var _nextDebugEventId = 0; - - final List _predefinedEnvironments = [ - 'https://app.dev.ramp-network.org', - 'https://app.demo.ramp.network', - 'https://app.rampnetwork.com', - ]; - - int _selectedEnvironment = 0; - String? _userEmailAddress; - String? _inAsset; - String? _inAssetValue; - String? _outAsset = 'BTC_BTC'; - String? _outAssetValue; - String? _enabledCryptoAssets; - String? _userAddress; - String? _hostAppName = 'Ramp Network Flutter'; - String? _hostApiKey; - TransactionFlow? _defaultFlow = TransactionFlow.ONRAMP; - List _enabledFlows = [TransactionFlow.ONRAMP, TransactionFlow.OFFRAMP, TransactionFlow.SWAP]; - - @override - void initState() { - _applyEnvironment(_selectedEnvironment); - super.initState(); - } - - @override - void dispose() { - _debugEvents.dispose(); - super.dispose(); - } - - void _selectEnvironment(int id) { - _applyEnvironment(id); - setState(() {}); - } - - void _applyEnvironment(int id) { - _selectedEnvironment = id; - } - - Configuration _buildConfiguration() { - final enabledCryptoAssets = _enabledCryptoAssets - ?.split(',') - .map((asset) => asset.trim()) - .where((asset) => asset.isNotEmpty) - .toList(); - return Configuration( - url: _predefinedEnvironments[_selectedEnvironment], - hostAppName: _hostAppName, - defaultFlow: _defaultFlow, - enabledFlows: List.from(_enabledFlows), - enabledCryptoAssets: enabledCryptoAssets, - inAsset: _inAsset, - inAssetValue: _inAssetValue, - outAsset: _outAsset, - outAssetValue: _outAssetValue, - useSendCryptoCallback: true, - hostApiKey: _hostApiKey, - userEmailAddress: _userEmailAddress, - userAddress: _userAddress, - ); - } - - void _addDebugEvent(String label, [Map data = const {}]) { - final encoded = const JsonEncoder.withIndent(' ').convert({'event': label, ...data}); - _debugEvents.value = [..._debugEvents.value, _DebugEvent(_nextDebugEventId++, encoded)]; - } - - void _removeDebugEvent(int id) { - _debugEvents.value = _debugEvents.value.where((e) => e.id != id).toList(growable: false); - } - - Future _showRamp(BuildContext context) async { - final ramp = RampFlutter(_buildConfiguration()); + final _signedUrlKey = GlobalKey(); + final _configurationKey = GlobalKey(); + var _launchMode = LaunchMode.signedUrl; + + Future _openRamp(BuildContext context) async { + final RampFlutter ramp; + try { + ramp = switch (_launchMode) { + LaunchMode.signedUrl => RampFlutter.signed(_signedUrlKey.currentState!.url), + LaunchMode.configuration => RampFlutter(_configurationKey.currentState!.configuration), + }; + } catch (error) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$error'))); + return; + } ramp.onWidgetEvent = (event) { + debugPrint('Ramp event: $event'); switch (event) { - case WidgetConfigDone(): - _addDebugEvent('WIDGET_CONFIG_DONE'); - case WidgetConfigFailed(): - _addDebugEvent('WIDGET_CONFIG_FAILED'); - case PurchaseCreated(:final payload): - _addDebugEvent('PURCHASE_CREATED', {'id': payload.purchase?.id, 'asset': payload.purchase?.asset?.symbol}); - case OfframpSaleCreated(:final payload): - _addDebugEvent('OFFRAMP_SALE_CREATED', {'id': payload.sale?.id}); - case SendCryptoRequested(:final payload): - _addDebugEvent('SEND_CRYPTO', { - 'address': payload.address, - 'amount': payload.amount, - 'asset': payload.assetInfo?.symbol, - }); - ramp.postHostEvent(SendCryptoResult.txHash('123')); + case SendCryptoRequested(): + ramp.postHostEvent(SendCryptoResult.txHash('demo-tx-hash')); case RequestCryptoAccount(:final payload): - _addDebugEvent('REQUEST_CRYPTO_ACCOUNT', {'type': payload.type, 'assetSymbol': payload.assetSymbol}); ramp.postHostEvent( - RequestCryptoAccountResult.account(address: '0xabc', type: payload.type, assetSymbol: payload.assetSymbol), + RequestCryptoAccountResult.account(address: '0xabc', type: payload.type), ); - case WidgetClose(:final payload): - _addDebugEvent('WIDGET_CLOSE', {'showAlert': payload.showAlert}); - case WidgetCloseRequest(): - _addDebugEvent('WIDGET_CLOSE_REQUEST'); + case WidgetClose(): + if (context.mounted) Navigator.of(context).maybePop(); + default: + break; } }; await showModalBottomSheet( context: context, - useRootNavigator: true, isScrollControlled: true, - enableDrag: true, - isDismissible: true, useSafeArea: true, - builder: (sheetContext) { - return SizedBox( - height: MediaQuery.sizeOf(sheetContext).height * 0.92, - child: Material( - color: Colors.white, - child: Stack( - children: [ - Column( - children: [ - const SizedBox( - height: 28, - child: Center( - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.all(Radius.circular(2)), - ), - child: SizedBox(width: 36, height: 4), - ), - ), - ), - Expanded(child: ramp.view), - ], - ), - Positioned( - left: 8, - right: 8, - top: 36, - child: _DebugEventList( - eventsListenable: _debugEvents, - maxHeight: MediaQuery.sizeOf(sheetContext).height * 0.45, - onDismiss: _removeDebugEvent, - ), - ), - ], - ), - ), - ); - }, + builder: (sheetContext) => SizedBox( + height: MediaQuery.sizeOf(sheetContext).height * 0.92, + child: ramp.view, + ), ); ramp.dispose(); @@ -181,181 +72,51 @@ class _RampFlutterAppState extends State { home: Builder( builder: (context) => Scaffold( appBar: AppBar(title: const Text('Ramp Network Flutter')), - body: Stack( + body: Column( children: [ Padding( - padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), - child: ListView(children: _formFields(context)), - ), - Positioned( - left: 8, - right: 8, - top: 8, - child: _DebugEventList( - eventsListenable: _debugEvents, - maxHeight: MediaQuery.sizeOf(context).height * 0.5, - onDismiss: _removeDebugEvent, + padding: const EdgeInsets.fromLTRB(16, 8, 16, 12), + child: SegmentedButton( + expandedInsets: EdgeInsets.zero, + showSelectedIcon: false, + segments: const [ + ButtonSegment(value: LaunchMode.signedUrl, label: Text('Signed URL')), + ButtonSegment(value: LaunchMode.configuration, label: Text('Configuration')), + ], + selected: {_launchMode}, + onSelectionChanged: (selection) { + setState(() => _launchMode = selection.single); + }, ), ), - ], - ), - ), - ), - ); - } - - List _formFields(BuildContext context) { - return [..._configurationForm(), _showRampButton(context), _appInfo()]; - } - - Widget _appInfo() { - return const Text('App version: Flutter WebView'); - } - - List _configurationForm() { - return [ - _segmentedControl('Env:', ['dev', 'demo', 'prod'], _selectEnvironment), - Text( - _predefinedEnvironments[_selectedEnvironment], - style: const TextStyle(color: Color.fromRGBO(46, 190, 117, 1)), - ), - _textField('User email address', (text) => _userEmailAddress = text, _userEmailAddress), - _textField('In asset', (text) => _inAsset = text, _inAsset), - _textField('In asset value', (text) => _inAssetValue = text, _inAssetValue), - _textField('Out asset', (text) => _outAsset = text, _outAsset), - _textField('Out asset value', (text) => _outAssetValue = text, _outAssetValue), - _textField('Enabled crypto assets', (text) => _enabledCryptoAssets = text, _enabledCryptoAssets), - _textField('User address', (text) => _userAddress = text, _userAddress), - _textField('Host app name', (text) => _hostAppName = text, _hostAppName), - _textField('Host API key', (text) => _hostApiKey = text, _hostApiKey), - _segmentedControl('Default flow:', ['ONRAMP', 'OFFRAMP'], (index) { - if (index == 0) { - _defaultFlow = TransactionFlow.ONRAMP; - } - if (index == 1) { - _defaultFlow = TransactionFlow.OFFRAMP; - } - setState(() {}); - }), - _enabledFlowsSection(), - ]; - } - - Widget _enabledFlowsSection() { - Widget flowSwitch(TransactionFlow flow) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(flow.name), - Switch( - value: _enabledFlows.contains(flow), - onChanged: (enabled) { - setState(() { - if (enabled) { - _enabledFlows = [..._enabledFlows, flow]; - } else { - _enabledFlows = _enabledFlows.where((value) => value != flow).toList(); - } - }); - }, - ), - ], - ); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Enabled flows:'), - Row( - children: [ - flowSwitch(TransactionFlow.ONRAMP), - flowSwitch(TransactionFlow.OFFRAMP), - flowSwitch(TransactionFlow.SWAP), - ], - ), - ], - ); - } - - Widget _showRampButton(BuildContext context) { - return TextButton(onPressed: () => _showRamp(context), child: const Text('Show Ramp')); - } - - Row _segmentedControl(String title, List options, void Function(int) itemSelected) { - final segments = options.asMap().entries.map((entry) { - return TextButton(onPressed: () => itemSelected(entry.key), child: Text(entry.value)); - }).toList(); - return Row(children: [Text(title), ...segments]); - } - - TextField _textField(String placeholder, void Function(String) onChanged, String? defaultValue) { - return TextField( - decoration: InputDecoration(hintText: placeholder), - onChanged: onChanged, - controller: TextEditingController(text: defaultValue), - ); - } -} - -class _DebugEvent { - _DebugEvent(this.id, this.body); - - final int id; - final String body; -} - -class _DebugEventList extends StatelessWidget { - const _DebugEventList({required this.eventsListenable, required this.maxHeight, required this.onDismiss}); - - final ValueNotifier> eventsListenable; - final double maxHeight; - final void Function(int id) onDismiss; - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder>( - valueListenable: eventsListenable, - builder: (context, events, _) { - if (events.isEmpty) { - return const SizedBox.shrink(); - } - return ConstrainedBox( - constraints: BoxConstraints(maxHeight: maxHeight), - child: ListView.separated( - shrinkWrap: true, - itemCount: events.length, - separatorBuilder: (context, index) => const SizedBox(height: 6), - itemBuilder: (context, index) { - final event = events[index]; - return Material( - elevation: 3, - borderRadius: BorderRadius.circular(8), - color: const Color(0xFF323232), + Expanded( child: Padding( - padding: const EdgeInsets.fromLTRB(12, 8, 4, 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + padding: const EdgeInsets.symmetric(horizontal: 16), + child: IndexedStack( + index: _launchMode == LaunchMode.signedUrl ? 0 : 1, children: [ - Expanded( - child: Text( - event.body, - style: const TextStyle(color: Colors.white, fontSize: 11, fontFamily: 'Courier'), - ), - ), - IconButton( - visualDensity: VisualDensity.compact, - icon: const Icon(Icons.close, color: Colors.white70, size: 18), - onPressed: () => onDismiss(event.id), - ), + SignedUrlForm(key: _signedUrlKey), + ConfigurationForm(key: _configurationKey), ], ), ), - ); - }, + ), + SafeArea( + top: false, + minimum: const EdgeInsets.fromLTRB(16, 12, 16, 16), + child: SizedBox( + width: double.infinity, + height: 52, + child: FilledButton( + onPressed: () => _openRamp(context), + child: const Text('Open Ramp', style: TextStyle(fontSize: 18)), + ), + ), + ), + ], ), - ); - }, + ), + ), ); } } diff --git a/example/lib/signed_url_form.dart b/example/lib/signed_url_form.dart new file mode 100644 index 0000000..d384f7f --- /dev/null +++ b/example/lib/signed_url_form.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +class SignedUrlForm extends StatefulWidget { + const SignedUrlForm({super.key, this.initialUrl = ''}); + + final String initialUrl; + + @override + State createState() => SignedUrlFormState(); +} + +class SignedUrlFormState extends State { + late final TextEditingController _controller = TextEditingController(text: widget.initialUrl); + + String get url => _controller.text.trim(); + + @override + void initState() { + super.initState(); + _controller.addListener(() => setState(() {})); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final parsed = Uri.tryParse(url); + final params = parsed?.queryParameters ?? const {}; + final timestampMs = int.tryParse(params['timestamp'] ?? ''); + + final checks = <(String, bool)>[ + ('https URL', parsed?.scheme == 'https'), + ('hostApiKey', (params['hostApiKey'] ?? '').isNotEmpty), + ( + timestampMs == null ? 'timestamp' : 'timestamp (${_formatLocal(timestampMs)})', + timestampMs != null, + ), + ('signature', (params['signature'] ?? '').isNotEmpty), + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + controller: _controller, + minLines: 6, + maxLines: 12, + keyboardType: TextInputType.url, + decoration: const InputDecoration( + labelText: 'Signed widget URL', + alignLabelWithHint: true, + border: OutlineInputBorder(), + hintText: 'https://app.dev.ramp-network.org/?hostApiKey=...×tamp=...&signature=...', + ), + ), + const SizedBox(height: 8), + for (final (label, ok) in checks) + Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + children: [ + Icon( + ok ? Icons.check_circle : Icons.cancel, + color: ok ? Colors.green : Colors.red, + size: 20, + ), + const SizedBox(width: 8), + Expanded(child: Text(label)), + ], + ), + ), + ], + ); + } + + String _formatLocal(int milliseconds) { + final local = DateTime.fromMillisecondsSinceEpoch(milliseconds).toLocal(); + String two(int n) => n.toString().padLeft(2, '0'); + return '${local.year}-${two(local.month)}-${two(local.day)} ' + '${two(local.hour)}:${two(local.minute)}:${two(local.second)}'; + } +} From 32ae23b66ef1558fbd1c4d0d5c6e3cd591f2f01a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Mon, 3 Aug 2026 20:41:22 +0200 Subject: [PATCH 49/56] add missing version event --- lib/src/widget_event.dart | 3 +++ lib/src/widget_events/app_version.dart | 20 ++++++++++++++++++++ test/ramp_webview_test.dart | 23 +++++++++++++++++------ 3 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 lib/src/widget_events/app_version.dart diff --git a/lib/src/widget_event.dart b/lib/src/widget_event.dart index 1409ff2..348acc7 100644 --- a/lib/src/widget_event.dart +++ b/lib/src/widget_event.dart @@ -8,6 +8,7 @@ export 'package:ramp_flutter/src/models/asset_info.dart'; export 'package:ramp_flutter/src/models/purchase_details.dart'; export 'package:ramp_flutter/src/models/sale_details.dart'; +part 'widget_events/app_version.dart'; part 'widget_events/offramp_sale_created.dart'; part 'widget_events/purchase_created.dart'; part 'widget_events/request_crypto_account.dart'; @@ -33,6 +34,8 @@ sealed class WidgetEvent { try { switch (type) { + case 'APP_VERSION': + return AppVersion(AppVersionPayload.fromJson(payload), widgetInstanceId: widgetInstanceId); case 'WIDGET_CONFIG_DONE': return WidgetConfigDone(widgetInstanceId: widgetInstanceId); case 'WIDGET_CONFIG_FAILED': diff --git a/lib/src/widget_events/app_version.dart b/lib/src/widget_events/app_version.dart new file mode 100644 index 0000000..22805c9 --- /dev/null +++ b/lib/src/widget_events/app_version.dart @@ -0,0 +1,20 @@ +part of '../widget_event.dart'; + +final class AppVersion extends WidgetEvent { + const AppVersion(this.payload, {super.widgetInstanceId}); + + final AppVersionPayload payload; +} + +class AppVersionPayload { + const AppVersionPayload({this.version}); + + final String? version; + + factory AppVersionPayload.fromJson(Map? json) { + if (json == null) { + return const AppVersionPayload(); + } + return AppVersionPayload(version: asString(json['version'])); + } +} diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index 975ce98..66c309d 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -99,6 +99,13 @@ void main() { final events = []; final ramp = RampFlutter.uri(widgetUrl)..onWidgetEvent = events.add; + ramp.handleJavaScriptMessage( + jsonEncode({ + 'type': 'APP_VERSION', + 'payload': {'version': '2.0'}, + 'widgetInstanceId': 'w0', + }), + ); ramp.handleJavaScriptMessage( jsonEncode({'type': 'WIDGET_CONFIG_DONE', 'payload': null, 'widgetInstanceId': 'w1'}), ); @@ -173,6 +180,7 @@ void main() { ramp.handleJavaScriptMessage(jsonEncode({'type': 'WIDGET_CLOSE_REQUEST', 'payload': null})); expect(events, [ + isA(), isA(), isA(), isA(), @@ -184,29 +192,32 @@ void main() { isA(), ]); - expect(events[0].widgetInstanceId, 'w1'); + final appVersion = events[0] as AppVersion; + expect(appVersion.widgetInstanceId, 'w0'); + expect(appVersion.payload.version, '2.0'); + expect(events[1].widgetInstanceId, 'w1'); - final purchase = events[2] as PurchaseCreated; + final purchase = events[3] as PurchaseCreated; expect(purchase.payload.purchase?.id, 'p1'); expect(purchase.payload.purchase?.asset?.symbol, 'ETH'); expect(purchase.payload.purchase?.fiatValue, 50.5); expect(purchase.payload.purchaseViewToken, 'token'); - final sale = events[3] as OfframpSaleCreated; + final sale = events[4] as OfframpSaleCreated; expect(sale.payload.sale?.id, 's1'); expect(sale.payload.sale?.fees?.currencySymbol, '978'); expect(sale.payload.saleViewToken, 'sale-token'); - final send = events[4] as SendCryptoRequested; + final send = events[5] as SendCryptoRequested; expect(send.payload.address, '0xabc'); expect(send.payload.assetInfo?.uai, 'eip155:1/slip44:60'); expect(send.payload.assetInfo?.symbol, 'ETH'); - final account = events[5] as RequestCryptoAccount; + final account = events[6] as RequestCryptoAccount; expect(account.payload.type, 'ETH'); expect(account.payload.assetSymbol, 'ETH'); - final close = events[6] as WidgetClose; + final close = events[7] as WidgetClose; expect(close.payload.showAlert, isTrue); expect(close.payload.descriptionText, 'Leave?'); }); From 356a9b6541619e97422aa5097530654dd1b69ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Mon, 3 Aug 2026 20:46:41 +0200 Subject: [PATCH 50/56] make send crypto protocol version a constant --- example/lib/configuration_form.dart | 3 +-- example/lib/main.dart | 9 ++------- example/lib/signed_url_form.dart | 11 ++--------- lib/src/configuration.dart | 3 ++- lib/src/widget_event.dart | 3 ++- 5 files changed, 9 insertions(+), 20 deletions(-) diff --git a/example/lib/configuration_form.dart b/example/lib/configuration_form.dart index 19ce7c5..bc4d6ff 100644 --- a/example/lib/configuration_form.dart +++ b/example/lib/configuration_form.dart @@ -56,8 +56,7 @@ class ConfigurationFormState extends State { _userAddress = TextEditingController(text: initial.userAddress ?? ''); _defaultFlow = initial.defaultFlow ?? TransactionFlow.ONRAMP; _enabledFlows = List.from( - initial.enabledFlows ?? - const [TransactionFlow.ONRAMP, TransactionFlow.OFFRAMP, TransactionFlow.SWAP], + initial.enabledFlows ?? const [TransactionFlow.ONRAMP, TransactionFlow.OFFRAMP, TransactionFlow.SWAP], ); } diff --git a/example/lib/main.dart b/example/lib/main.dart index 21f556a..3b0dae4 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -43,9 +43,7 @@ class _RampFlutterAppState extends State { case SendCryptoRequested(): ramp.postHostEvent(SendCryptoResult.txHash('demo-tx-hash')); case RequestCryptoAccount(:final payload): - ramp.postHostEvent( - RequestCryptoAccountResult.account(address: '0xabc', type: payload.type), - ); + ramp.postHostEvent(RequestCryptoAccountResult.account(address: '0xabc', type: payload.type)); case WidgetClose(): if (context.mounted) Navigator.of(context).maybePop(); default: @@ -57,10 +55,7 @@ class _RampFlutterAppState extends State { context: context, isScrollControlled: true, useSafeArea: true, - builder: (sheetContext) => SizedBox( - height: MediaQuery.sizeOf(sheetContext).height * 0.92, - child: ramp.view, - ), + builder: (sheetContext) => SizedBox(height: MediaQuery.sizeOf(sheetContext).height * 0.92, child: ramp.view), ); ramp.dispose(); diff --git a/example/lib/signed_url_form.dart b/example/lib/signed_url_form.dart index d384f7f..a544691 100644 --- a/example/lib/signed_url_form.dart +++ b/example/lib/signed_url_form.dart @@ -35,10 +35,7 @@ class SignedUrlFormState extends State { final checks = <(String, bool)>[ ('https URL', parsed?.scheme == 'https'), ('hostApiKey', (params['hostApiKey'] ?? '').isNotEmpty), - ( - timestampMs == null ? 'timestamp' : 'timestamp (${_formatLocal(timestampMs)})', - timestampMs != null, - ), + (timestampMs == null ? 'timestamp' : 'timestamp (${_formatLocal(timestampMs)})', timestampMs != null), ('signature', (params['signature'] ?? '').isNotEmpty), ]; @@ -63,11 +60,7 @@ class SignedUrlFormState extends State { padding: const EdgeInsets.symmetric(vertical: 2), child: Row( children: [ - Icon( - ok ? Icons.check_circle : Icons.cancel, - color: ok ? Colors.green : Colors.red, - size: 20, - ), + Icon(ok ? Icons.check_circle : Icons.cancel, color: ok ? Colors.green : Colors.red, size: 20), const SizedBox(width: 8), Expanded(child: Text(label)), ], diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index f179df9..c8bc188 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -28,6 +28,7 @@ class Configuration { static const String defaultUrl = 'https://app.rampnetwork.com'; static const String sdkType = 'FLUTTER'; static const String sdkVersion = '5.0.0'; + static const int sendCryptoProtocolVersion = 1; final String? url; final TransactionFlow? defaultFlow; @@ -83,7 +84,7 @@ class Configuration { if (_nonEmpty(userAddress)) 'userAddress': userAddress!, if (_nonEmpty(userEmailAddress)) 'userEmailAddress': userEmailAddress!, if (_nonEmpty(webhookStatusUrl)) 'webhookStatusUrl': webhookStatusUrl!, - if (useSendCryptoCallback == true) 'useSendCryptoCallbackVersion': '1', + if (useSendCryptoCallback == true) 'useSendCryptoCallbackVersion': sendCryptoProtocolVersion.toString(), 'sdkType': sdkType, 'sdkVersion': sdkVersion, }, diff --git a/lib/src/widget_event.dart b/lib/src/widget_event.dart index 348acc7..583199f 100644 --- a/lib/src/widget_event.dart +++ b/lib/src/widget_event.dart @@ -1,4 +1,5 @@ import 'package:flutter/foundation.dart'; +import 'package:ramp_flutter/src/configuration.dart'; import 'package:ramp_flutter/src/models/asset_info.dart'; import 'package:ramp_flutter/src/models/json_map.dart'; import 'package:ramp_flutter/src/models/purchase_details.dart'; @@ -46,7 +47,7 @@ sealed class WidgetEvent { return OfframpSaleCreated(OfframpSaleCreatedPayload.fromJson(payload), widgetInstanceId: widgetInstanceId); case 'SEND_CRYPTO': final version = json['eventVersion']; - if (version != null && version != 1) { + if (version != null && version != Configuration.sendCryptoProtocolVersion) { debugPrint('RampFlutter: drop SEND_CRYPTO — unsupported eventVersion=$version'); return null; } From 2eb271347fb3d9116f515f0d3cfd60647ec04520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Mon, 3 Aug 2026 20:49:39 +0200 Subject: [PATCH 51/56] improve make commands --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index e789c3b..74e4725 100644 --- a/Makefile +++ b/Makefile @@ -9,10 +9,10 @@ analyze: format: dart format --line-length 120 lib test example/lib -test: +test: get analyze format flutter test -build: +build: get cd example && flutter build ios --debug --no-codesign cd example && flutter build apk --debug From 51edf4bfbb6af90c88298a9e12c456e7ec49369c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Mon, 3 Aug 2026 21:01:18 +0200 Subject: [PATCH 52/56] update docs --- MIGRATION.md | 2 ++ README.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/MIGRATION.md b/MIGRATION.md index c8d0497..e4018b9 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -52,6 +52,8 @@ has finished loading. ```dart ramp.onWidgetEvent = (event) { switch (event) { + case AppVersion(): + break; case PurchaseCreated(:final payload): break; case OfframpSaleCreated(:final payload): diff --git a/README.md b/README.md index e8926a3..26e8939 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ final ramp = RampFlutter( ) ..onWidgetEvent = (event) { switch (event) { + case AppVersion(): + break; case PurchaseCreated(:final payload): break; case OfframpSaleCreated(:final payload): From 7ac7a656c8132bfcf53de758683a6c273ae9fc17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Tue, 4 Aug 2026 13:42:30 +0200 Subject: [PATCH 53/56] improve ios sim select in ios example app --- Makefile | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 74e4725..7e4dae7 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,16 @@ -.PHONY: get analyze format test ios android +.PHONY: get format analyze test ios +.DEFAULT_GOAL := test get: flutter pub get +format: + dart format lib test example/lib + analyze: dart analyze -format: - dart format --line-length 120 lib test example/lib - -test: get analyze format +test: get format analyze flutter test build: get @@ -17,5 +18,7 @@ build: get cd example && flutter build apk --debug ios: - flutter emulators --launch apple_ios_simulator - cd example && flutter run -d "iPhone" + @id=$$(flutter devices --device-connection attached | grep SimRuntime.iOS \ + | head -n1 | cut -d'•' -f2 | xargs); \ + [ -n "$$id" ] || (echo "No iOS device found" && exit 1); \ + cd example && flutter run -d $$id From 7a4b48ba993cf6f72bcd489cb4bf436da3516754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Tue, 4 Aug 2026 13:43:57 +0200 Subject: [PATCH 54/56] add theme support in example app --- example/lib/main.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index 3b0dae4..8460cd3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -64,6 +64,15 @@ class _RampFlutterAppState extends State { @override Widget build(BuildContext context) { return MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo, brightness: Brightness.light), + useMaterial3: true, + ), + darkTheme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo, brightness: Brightness.dark), + useMaterial3: true, + ), + themeMode: ThemeMode.system, home: Builder( builder: (context) => Scaffold( appBar: AppBar(title: const Text('Ramp Network Flutter')), From 91487987cea5fdd51a3928fd40057892f4466b26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Tue, 4 Aug 2026 14:31:32 +0200 Subject: [PATCH 55/56] drop partner name/logo from configuration --- MIGRATION.md | 2 +- README.md | 5 ++++- example/lib/configuration_form.dart | 6 ------ lib/src/configuration.dart | 3 --- test/ramp_webview_test.dart | 2 -- 5 files changed, 5 insertions(+), 13 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index e4018b9..0b5c0e8 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -88,7 +88,7 @@ point. It builds widget URLs with widget-2 params and always appends | `defaultAsset` / `fiatCurrency` / `fiatValue` / `swapAsset` / `offrampAsset` / `swapAmount` | `inAsset` / `outAsset` / `inAssetValue` / `outAssetValue` / `enabledCryptoAssets` | | `enabledFlows` / `defaultFlow` as strings | `TransactionFlow` enum | | — | `paymentMethodType` (`PaymentMethodType` enum) | -| `hostLogoUrl`, `containerNode`, `deepLinkScheme`, `variant` | removed | +| `hostLogoUrl`, `hostAppName`, `containerNode`, `deepLinkScheme`, `variant` | removed — partner name and logo are set by Ramp Network on the integration (`PARTNER_NAME` / `PARTNER_LOGO_URL`) | Default base URL is `https://app.rampnetwork.com`. diff --git a/README.md b/README.md index 26e8939..bdeb27c 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,6 @@ import 'package:ramp_flutter/ramp_flutter.dart'; final ramp = RampFlutter( Configuration( hostApiKey: 'YOUR_API_KEY', - hostAppName: 'My App', enabledFlows: [TransactionFlow.ONRAMP, TransactionFlow.OFFRAMP], outAsset: 'BTC_BTC', ), @@ -87,6 +86,10 @@ final ramp = RampFlutter.signed(signedWidgetUrlFromYourBackend) Generate the signed URL on your backend (`hostApiKey`, `timestamp`, `signature`). Keep signing keys out of the app. +Partner **name** and **logo** are not SDK configuration fields. Ask Ramp +Network to set host features `PARTNER_NAME` / `PARTNER_LOGO_URL` for your +integration (for example via integrations@rampnetwork.com). + For more configuration parameters see [Ramp Network Flutter documentation](https://docs.ramp.network/mobile/flutter-sdk/). diff --git a/example/lib/configuration_form.dart b/example/lib/configuration_form.dart index bc4d6ff..3007227 100644 --- a/example/lib/configuration_form.dart +++ b/example/lib/configuration_form.dart @@ -9,7 +9,6 @@ class ConfigurationForm extends StatefulWidget { static Configuration defaults() => const Configuration( url: 'https://app.dev.ramp-network.org', - hostAppName: 'Ramp Network Flutter', enabledFlows: [TransactionFlow.ONRAMP, TransactionFlow.OFFRAMP, TransactionFlow.SWAP], defaultFlow: TransactionFlow.ONRAMP, outAsset: 'BTC_BTC', @@ -28,7 +27,6 @@ class ConfigurationFormState extends State { ]; late final TextEditingController _hostApiKey; - late final TextEditingController _hostAppName; late final TextEditingController _inAsset; late final TextEditingController _inAssetValue; late final TextEditingController _outAsset; @@ -47,7 +45,6 @@ class ConfigurationFormState extends State { final initial = widget.initialConfiguration ?? ConfigurationForm.defaults(); _url = initial.url ?? _environments.first; _hostApiKey = TextEditingController(text: initial.hostApiKey ?? ''); - _hostAppName = TextEditingController(text: initial.hostAppName ?? ''); _inAsset = TextEditingController(text: initial.inAsset ?? ''); _inAssetValue = TextEditingController(text: initial.inAssetValue ?? ''); _outAsset = TextEditingController(text: initial.outAsset ?? ''); @@ -62,7 +59,6 @@ class ConfigurationFormState extends State { List get _textControllers => [ _hostApiKey, - _hostAppName, _inAsset, _inAssetValue, _outAsset, @@ -87,7 +83,6 @@ class ConfigurationFormState extends State { Configuration _build() => Configuration( url: _url, hostApiKey: _trimmed(_hostApiKey), - hostAppName: _trimmed(_hostAppName), defaultFlow: _defaultFlow, enabledFlows: List.from(_enabledFlows), inAsset: _trimmed(_inAsset), @@ -127,7 +122,6 @@ class ConfigurationFormState extends State { ), const SizedBox(height: 12), _field(_hostApiKey, 'Host API key'), - _field(_hostAppName, 'Host app name'), const Text('Enabled flows'), Row( children: [ diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index c8bc188..73b8f08 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -15,7 +15,6 @@ class Configuration { this.outAssetValue, this.finalUrl, this.hostApiKey, - this.hostAppName, this.offrampWebhookV3Url, this.paymentMethodType, this.selectedCountryCode, @@ -40,7 +39,6 @@ class Configuration { final String? outAssetValue; final String? finalUrl; final String? hostApiKey; - final String? hostAppName; final String? offrampWebhookV3Url; final PaymentMethodType? paymentMethodType; final String? selectedCountryCode; @@ -77,7 +75,6 @@ class Configuration { if (_nonEmpty(outAssetValue)) 'outAssetValue': outAssetValue!, if (_nonEmpty(finalUrl)) 'finalUrl': finalUrl!, if (_nonEmpty(hostApiKey)) 'hostApiKey': hostApiKey!, - if (_nonEmpty(hostAppName)) 'hostAppName': hostAppName!, if (_nonEmpty(offrampWebhookV3Url)) 'offrampWebhookV3Url': offrampWebhookV3Url!, if (paymentMethodType != null) 'paymentMethodType': paymentMethodType!.name, if (_nonEmpty(selectedCountryCode)) 'selectedCountryCode': selectedCountryCode!, diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index 66c309d..30d2b3f 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -19,7 +19,6 @@ void main() { final url = const Configuration( url: 'https://app.dev.ramp-network.org/custom', hostApiKey: 'key', - hostAppName: 'App', enabledCryptoAssets: ['ETH_*', 'BTC_BTC'], inAsset: 'EUR', outAsset: 'ETH_ETH', @@ -40,7 +39,6 @@ void main() { expect(url.host, 'app.dev.ramp-network.org'); expect(url.path, '/custom'); expect(url.queryParameters['hostApiKey'], 'key'); - expect(url.queryParameters['hostAppName'], 'App'); expect(url.queryParameters['enabledFlows'], 'ONRAMP,OFFRAMP'); expect(url.queryParameters['enabledCryptoAssets'], 'ETH_*,BTC_BTC'); expect(url.queryParameters['inAsset'], 'EUR'); From 63b6f36420bcc93ff1f2fc19484a8c633dbe91a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Jab=C5=82on=CC=81ski?= Date: Tue, 4 Aug 2026 14:57:57 +0200 Subject: [PATCH 56/56] remove finalUrl param --- MIGRATION.md | 1 + lib/src/configuration.dart | 3 --- test/ramp_webview_test.dart | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 0b5c0e8..6fa4487 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -89,6 +89,7 @@ point. It builds widget URLs with widget-2 params and always appends | `enabledFlows` / `defaultFlow` as strings | `TransactionFlow` enum | | — | `paymentMethodType` (`PaymentMethodType` enum) | | `hostLogoUrl`, `hostAppName`, `containerNode`, `deepLinkScheme`, `variant` | removed — partner name and logo are set by Ramp Network on the integration (`PARTNER_NAME` / `PARTNER_LOGO_URL`) | +| `finalUrl` | removed — hosted-mode redirect only; not used by the Flutter WebView SDK | Default base URL is `https://app.rampnetwork.com`. diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index 73b8f08..1b82503 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -13,7 +13,6 @@ class Configuration { this.inAssetValue, this.outAsset, this.outAssetValue, - this.finalUrl, this.hostApiKey, this.offrampWebhookV3Url, this.paymentMethodType, @@ -37,7 +36,6 @@ class Configuration { final String? inAssetValue; final String? outAsset; final String? outAssetValue; - final String? finalUrl; final String? hostApiKey; final String? offrampWebhookV3Url; final PaymentMethodType? paymentMethodType; @@ -73,7 +71,6 @@ class Configuration { if (_nonEmpty(inAssetValue)) 'inAssetValue': inAssetValue!, if (_nonEmpty(outAsset)) 'outAsset': outAsset!, if (_nonEmpty(outAssetValue)) 'outAssetValue': outAssetValue!, - if (_nonEmpty(finalUrl)) 'finalUrl': finalUrl!, if (_nonEmpty(hostApiKey)) 'hostApiKey': hostApiKey!, if (_nonEmpty(offrampWebhookV3Url)) 'offrampWebhookV3Url': offrampWebhookV3Url!, if (paymentMethodType != null) 'paymentMethodType': paymentMethodType!.name, diff --git a/test/ramp_webview_test.dart b/test/ramp_webview_test.dart index 30d2b3f..84b5006 100644 --- a/test/ramp_webview_test.dart +++ b/test/ramp_webview_test.dart @@ -24,7 +24,6 @@ void main() { outAsset: 'ETH_ETH', inAssetValue: '10000', outAssetValue: '500000', - finalUrl: 'https://example.com/done', webhookStatusUrl: 'https://example.com/purchase-hook', offrampWebhookV3Url: 'https://example.com/hook', selectedCountryCode: 'PL', @@ -45,7 +44,6 @@ void main() { expect(url.queryParameters['outAsset'], 'ETH_ETH'); expect(url.queryParameters['inAssetValue'], '10000'); expect(url.queryParameters['outAssetValue'], '500000'); - expect(url.queryParameters['finalUrl'], 'https://example.com/done'); expect(url.queryParameters['webhookStatusUrl'], 'https://example.com/purchase-hook'); expect(url.queryParameters['offrampWebhookV3Url'], 'https://example.com/hook'); expect(url.queryParameters['selectedCountryCode'], 'PL');