diff --git a/.gitignore b/.gitignore index ac5aa98..372f50c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,10 @@ 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/ +.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 c09dfb1..38408b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 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: 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 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 * Updated package documentation @@ -10,7 +23,7 @@ ## 3.0.0 -* Regenerated plugin to fix platform specific dependencies +* Regenerated plugin to fix platform specific dependencies ## 2.0.2 diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..6fa4487 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,114 @@ +# 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 AppVersion(): + break; + 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`, `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`. + +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/Makefile b/Makefile new file mode 100644 index 0000000..7e4dae7 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +.PHONY: get format analyze test ios +.DEFAULT_GOAL := test + +get: + flutter pub get + +format: + dart format lib test example/lib + +analyze: + dart analyze + +test: get format analyze + flutter test + +build: get + cd example && flutter build ios --debug --no-codesign + cd example && flutter build apk --debug + +ios: + @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 diff --git a/README.md b/README.md index 367853c..bdeb27c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,107 @@ # Ramp Network Flutter -Official Flutter wrapper for Ramp Network +Official Flutter package 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 +``` + +Upgrading from 4.x? See [MIGRATION.md](MIGRATION.md). + +### Host setup + +- **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 + +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`): + +```dart +import 'package:ramp_flutter/ramp_flutter.dart'; + +final ramp = RampFlutter( + Configuration( + hostApiKey: 'YOUR_API_KEY', + enabledFlows: [TransactionFlow.ONRAMP, TransactionFlow.OFFRAMP], + outAsset: 'BTC_BTC', + ), +) + ..onWidgetEvent = (event) { + switch (event) { + case AppVersion(): + break; + 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; + } + }; + +await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => SizedBox( + height: MediaQuery.sizeOf(context).height * 0.92, + child: ramp.view, + ), +); + +ramp.dispose(); +``` + +**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. + +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/). + +### Notes + +- Off-widget navigations (other https hosts, custom schemes, `intent:`) and + `target=_blank` / `window.open` open in the system browser via + `NavigationDelegate` and a forked `webview_flutter` + ([mateusz-ramp/flutter_packages](https://github.com/mateusz-ramp/flutter_packages/tree/webview-target-blank) + / 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` for capture). Host apps must declare camera / photo library + usage (see Getting Started). iOS WKWebView handles file inputs natively. 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/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 61d196e..0000000 --- a/android/build.gradle +++ /dev/null @@ -1,55 +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() - maven { url 'https://jitpack.io' } - } -} - -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 21 - } - - dependencies { - implementation 'com.github.RampNetwork:ramp-sdk-android:4.0.+' - } -} 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 1138f52..0000000 --- a/android/src/main/kotlin/network/ramp/ramp_flutter/RampFlutterPlugin.kt +++ /dev/null @@ -1,185 +0,0 @@ -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 - - 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, - ) - ) -} diff --git a/example/.gitignore b/example/.gitignore index 29a3a50..3820a95 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 @@ -25,11 +27,11 @@ migrate_working_dir/ **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ -.flutter-plugins .flutter-plugins-dependencies .pub-cache/ .pub/ /build/ +/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 1f062de..f7ea7d8 100644 --- a/example/README.md +++ b/example/README.md @@ -1,16 +1,8 @@ -# ramp_flutter_example +# Ramp Network Flutter example -Demonstrates how to use the ramp_flutter plugin. +Demo app for [`ramp_flutter`](../): configure widget URL params, present the +Ramp WebView in a bottom sheet, and log `WidgetEvent` / host replies. -## 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. +```sh +flutter run +``` diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 0d29021..76940e6 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1,28 +1,4 @@ -# 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 +formatter: + page_width: 120 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 9f25e16..0000000 --- a/example/android/app/build.gradle +++ /dev/null @@ -1,64 +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 21 - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - implementation 'com.github.RampNetwork:ramp-sdk-android:3.+' -} \ 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 24b215d..5f46e37 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,12 +1,15 @@ + + + + + + + + + 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 aeb112b..0000000 --- a/example/android/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - maven { url 'https://jitpack.io' } - } - - 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/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/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 7bcd316..0000000 --- a/example/ios/Podfile +++ /dev/null @@ -1,42 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '12.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! - pod 'Ramp', :git => 'https://github.com/RampNetwork/ramp-sdk-ios', :tag => '4.0.2' - - 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 2bede9f..0000000 --- a/example/ios/Podfile.lock +++ /dev/null @@ -1,40 +0,0 @@ -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 - -PODFILE CHECKSUM: 15079d41652eeb2a8525b2965db898b1e2e9e3ca - -COCOAPODS: 1.14.3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 7593176..3a7fedd 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -10,12 +10,16 @@ 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 */; }; - 93025B5D7BBBFC1A8516BDCD /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 089427D76D90C916BF2DEB27 /* Pods_Runner.framework */; }; + 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 */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -30,14 +34,13 @@ /* 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 = ""; }; + 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 = ""; }; @@ -46,7 +49,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 */ @@ -54,24 +56,17 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 93025B5D7BBBFC1A8516BDCD /* Pods_Runner.framework in Frameworks */, + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage 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 = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -86,8 +81,6 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, - AE0A181C66D88E3021D74FEF /* Pods */, - 0ED26C7AA03001342DE76ABE /* Frameworks */, ); sourceTree = ""; }; @@ -109,21 +102,12 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); 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 */ @@ -131,20 +115,21 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 09BBB8AE4928621E4B8C77B3 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 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"; @@ -156,7 +141,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -174,6 +159,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -198,28 +186,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; @@ -236,23 +202,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; @@ -277,11 +226,15 @@ files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -306,6 +259,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++"; @@ -325,7 +279,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; @@ -336,6 +289,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; @@ -344,7 +298,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; @@ -360,7 +314,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 = ( @@ -379,6 +333,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++"; @@ -398,7 +353,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; @@ -409,6 +363,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; @@ -423,7 +378,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; @@ -435,6 +390,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++"; @@ -454,7 +410,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; @@ -465,6 +420,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; @@ -473,7 +429,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; @@ -491,7 +447,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 = ( @@ -514,7 +470,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 = ( @@ -553,6 +509,20 @@ 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/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.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index e25afce..5db441f 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,13 +1,31 @@ + + + + + + + + + + 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 @@ - - 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 +} 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/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/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 88b7085..2dd3b26 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -42,9 +42,30 @@ 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. + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName @@ -54,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/lib/configuration_form.dart b/example/lib/configuration_form.dart new file mode 100644 index 0000000..3007227 --- /dev/null +++ b/example/lib/configuration_form.dart @@ -0,0 +1,176 @@ +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', + 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 _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 ?? ''); + _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, + _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), + 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'), + 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 37389b4..8460cd3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,307 +1,126 @@ 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 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'configuration_form.dart'; +import 'signed_url_form.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'); - }); -} +enum LaunchMode { configuration, signedUrl } class RampFlutterApp extends StatefulWidget { - const RampFlutterApp({Key? key}) : super(key: key); + const RampFlutterApp({super.key}); @override State createState() => _RampFlutterAppState(); } class _RampFlutterAppState extends State { - final ramp = RampFlutter(); - final Configuration _configuration = Configuration(); - - final List _predefinedEnvironments = [ - "https://app.dev.ramp-network.org", - "https://ri-widget-staging.firebaseapp.com", - "https://app.ramp.network", - ]; - - int _selectedEnvironment = 1; - bool _useFullCustomUrl = false; - String _fullCustomUrl = ""; - - @override - void initState() { - _configuration.hostAppName = "Ramp Network Flutter"; - _configuration.url = _predefinedEnvironments[_selectedEnvironment]; - _configuration.enabledFlows = ["ONRAMP", "OFFRAMP"]; - _configuration.useSendCryptoCallback = true; - - ramp.onOnrampPurchaseCreated = onOnrampPurchaseCreated; - ramp.onSendCryptoRequested = onSendCryptoRequested; - ramp.onOfframpSaleCreated = onOfframpSaleCreated; - ramp.onRampClosed = onRampClosed; - - super.initState(); - } - - void _selectEnvironment(int id) { - _selectedEnvironment = id; - _configuration.url = _predefinedEnvironments[_selectedEnvironment]; - setState(() => {}); - } - - void onOnrampPurchaseCreated( - OnrampPurchase purchase, - String purchaseViewToken, - String apiUrl, - ) { - _showNotification("Ramp Network Notification", "onramp purchase created"); - } - - void onSendCryptoRequested(SendCryptoPayload payload) { - _showNotification("Ramp Network Notification", "send crypto requested"); - ramp.sendCrypto("123"); - } + 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; + } - void onOfframpSaleCreated( - OfframpSale sale, - String saleViewToken, - String apiUrl, - ) { - _showNotification("Ramp Network Notification", "offramp sale created"); - } + ramp.onWidgetEvent = (event) { + debugPrint('Ramp event: $event'); + switch (event) { + case SendCryptoRequested(): + ramp.postHostEvent(SendCryptoResult.txHash('demo-tx-hash')); + case RequestCryptoAccount(:final payload): + ramp.postHostEvent(RequestCryptoAccountResult.account(address: '0xabc', type: payload.type)); + case WidgetClose(): + if (context.mounted) Navigator.of(context).maybePop(); + default: + break; + } + }; + + await showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + builder: (sheetContext) => SizedBox(height: MediaQuery.sizeOf(sheetContext).height * 0.92, child: ramp.view), + ); - void onRampClosed() { - _showNotification("Ramp Network Notification", "ramp closed"); + ramp.dispose(); } @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), - ), - ), + return MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo, brightness: Brightness.light), + useMaterial3: true, ), - ); - } - - 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; - } - - Widget _appInfo() { - return PlatformText("App version: Flutter"); - } - - List _customUrlForm() { - return [ - _textField( - "Full custom URL", - (text) => _fullCustomUrl = text, - _fullCustomUrl, - ) - ]; - } - - List _configurationForm() { - return [ - _segmentedControl( - "Env:", - ["dev", "staging", "prod"], - _selectEnvironment, + darkTheme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo, brightness: Brightness.dark), + useMaterial3: true, ), - PlatformText( - _predefinedEnvironments[_selectedEnvironment], - style: const TextStyle( - color: Color.fromRGBO(46, 190, 117, 1), + themeMode: ThemeMode.system, + home: Builder( + builder: (context) => Scaffold( + appBar: AppBar(title: const Text('Ramp Network Flutter')), + body: Column( + children: [ + Padding( + 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); + }, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: IndexedStack( + index: _launchMode == LaunchMode.signedUrl ? 0 : 1, + children: [ + 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)), + ), + ), + ), + ], + ), ), ), - _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( - "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(), - ]; - } - - 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(() => {}); - }, - ); - return Row(children: [ - PlatformText("Enabled: "), - PlatformText("ONRAMP"), - onRamp, - PlatformText("OFFRAMP"), - offRamp, - ]); - } - - Widget _showRampButton() { - return PlatformTextButton( - onPressed: () { - if (_useFullCustomUrl) { - Configuration c = Configuration(); - c.url = _fullCustomUrl; - ramp.showRamp(c); - } else { - ramp.showRamp(_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) { - return PlatformTextButton( - onPressed: () => itemSelected(entry.key), - child: PlatformText(entry.value), - ); - }).toList(); - List children = [PlatformText(title)]; - children.addAll(segments); - return Row(children: children); - } - - PlatformTextField _textField( - String placeholder, - void Function(String) onChanged, - String? defaultValue, - ) { - return PlatformTextField( - hintText: placeholder, - onChanged: onChanged, - 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/lib/signed_url_form.dart b/example/lib/signed_url_form.dart new file mode 100644 index 0000000..a544691 --- /dev/null +++ b/example/lib/signed_url_form.dart @@ -0,0 +1,79 @@ +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)}'; + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 9707c6d..0e41e9b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,58 +5,98 @@ packages: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.5.0" - characters: + version: "2.7.0" + async: dependency: transitive description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 url: "https://pub.dev" source: hosted - version: "1.3.0" - clock: + version: "2.13.1" + characters: dependency: transitive description: - name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.4.1" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" - cupertino_icons: - dependency: "direct main" + version: "1.19.1" + cross_file: + dependency: transitive description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + name: cross_file + sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "0.3.5+4" dbus: dependency: transitive description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.14" ffi: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.2" + 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 @@ -66,82 +106,151 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "2.0.3" - flutter_local_notifications: - dependency: "direct main" + 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_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: transitive description: - name: flutter_local_notifications - sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00" + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "16.3.3" - flutter_local_notifications_linux: + version: "1.6.0" + http_parser: dependency: transitive description: - name: flutter_local_notifications_linux - sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.1" - flutter_local_notifications_platform_interface: + version: "4.1.2" + image_picker: dependency: transitive description: - name: flutter_local_notifications_platform_interface - sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" + name: image_picker + sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667 url: "https://pub.dev" source: hosted - version: "7.2.0" - flutter_platform_widgets: - dependency: "direct main" + version: "1.2.3" + image_picker_android: + dependency: transitive description: - name: flutter_platform_widgets - sha256: c483c0591d845d2adb84e341a1cfb746f1a8a7aff4c72a5957772446020601f4 + name: image_picker_android + sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a" url: "https://pub.dev" source: hosted - version: "6.1.0" + 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" lints: 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: 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" + 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: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" petitparser: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "7.0.2" plugin_platform_interface: dependency: transitive description: @@ -156,44 +265,176 @@ 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" - timezone: + version: "0.0.0" + source_span: dependency: transitive description: - name: timezone - sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "0.9.4" + version: "1.10.2" + 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" + 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: + 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: "2.1.4" - xdg_directories: + version: "1.1.1" + webview_flutter: + dependency: transitive + 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: transitive + 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: transitive + 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: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "5.15.0" xml: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.6.1" sdks: - dart: ">=3.3.0-279.1.beta <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.12.2 <4.0.0" + flutter: ">=3.44.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 2351647..783e8f2 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,22 +1,19 @@ name: ramp_flutter_example -description: "Demonstrates how to use the ramp_flutter plugin." -version: 1.0.0+1 -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +description: "Demonstrates how to use the ramp_flutter package." +publish_to: 'none' +version: 0.1.0+1 environment: - sdk: '>=3.2.3 <4.0.0' + sdk: ^3.12.2 dependencies: flutter: sdk: flutter ramp_flutter: 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 + flutter_lints: ^6.0.0 flutter: uses-material-design: true 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 f503dbd..0000000 --- a/ios/Classes/RampFlutterPlugin.swift +++ /dev/null @@ -1,176 +0,0 @@ -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) - } - } -} diff --git a/ios/ramp_flutter.podspec b/ios/ramp_flutter.podspec deleted file mode 100644 index 55a3e4a..0000000 --- a/ios/ramp_flutter.podspec +++ /dev/null @@ -1,22 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'ramp_flutter' - s.version = '1.0.1' - s.summary = 'Ramp Network iOS wrapper for Flutter.' - 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. - 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.dependency 'Ramp' - s.platform = :ios, '11.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/lib/configuration.dart b/lib/configuration.dart deleted file mode 100644 index 47836ac..0000000 --- a/lib/configuration.dart +++ /dev/null @@ -1,52 +0,0 @@ -class Configuration { - // main URL - String? url; - - // query params - String? containerNode; - String? deepLinkScheme; - String? defaultAsset; - String? defaultFlow; - List? enabledFlows; - String? fiatCurrency; - String? fiatValue; - String? finalUrl; - String? hostApiKey; - String? hostAppName; - String? hostLogoUrl; - String? offrampWebhookV3Url; - String? selectedCountryCode; - String? swapAmount; - String? swapAsset; - String? userAddress; - String? userEmailAddress; - bool? useSendCryptoCallback; - 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, - }; - } -} diff --git a/lib/offramp_sale.dart b/lib/offramp_sale.dart deleted file mode 100644 index 05080d9..0000000 --- a/lib/offramp_sale.dart +++ /dev/null @@ -1,57 +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(); - 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(); - 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(); - fiat.amount = arguments["amount"]; - fiat.currencySymbol = arguments["currencySymbol"]; - return fiat; - } -} diff --git a/lib/onramp_purchase.dart b/lib/onramp_purchase.dart deleted file mode 100644 index 2e6d79b..0000000 --- a/lib/onramp_purchase.dart +++ /dev/null @@ -1,61 +0,0 @@ -class OnrampPurchase { - String? id; - String? endTime; - PurchaseAssetInfo? asset; - String? receiverAddress; - String? cryptoAmount; - String? fiatCurrency; - int? 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 = arguments["fiatValue"]; - purchase.assetExchangeRate = arguments["assetExchangeRate"]; - purchase.baseRampFee = arguments["baseRampFee"]; - purchase.networkFee = arguments["networkFee"]; - purchase.appliedFee = 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; - } -} - -class PurchaseAssetInfo { - String? address; - int? decimals; - String? name; - String? symbol; - String? type; - - static PurchaseAssetInfo fromArguments(dynamic arguments) { - PurchaseAssetInfo assetInfo = PurchaseAssetInfo(); - 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 be3be42..e4f57de 100644 --- a/lib/ramp_flutter.dart +++ b/lib/ramp_flutter.dart @@ -1,72 +1,38 @@ -import 'dart:async'; +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'; -import 'package:flutter/services.dart'; -import 'package:ramp_flutter/offramp_sale.dart'; -import 'package:ramp_flutter/onramp_purchase.dart'; -import 'package:ramp_flutter/send_crypto_payload.dart'; +export 'package:ramp_flutter/src/configuration.dart'; +export 'package:ramp_flutter/src/host_event.dart'; +export 'package:ramp_flutter/src/widget_event.dart'; -import 'configuration.dart'; - -/// Wrapper class for Ramp Network Flutter widget class RampFlutter { - final MethodChannel _channel = const MethodChannel('ramp_flutter'); + RampFlutter(Configuration configuration) : this._(configuration.buildWidgetUrl()); - Function(OnrampPurchase, String, String)? onOnrampPurchaseCreated; - Function(SendCryptoPayload payload)? onSendCryptoRequested; - Function(OfframpSale, String, String)? onOfframpSaleCreated; - Function()? onRampClosed; + factory RampFlutter.signed(String url) => RampFlutter._(validateRampSignedUrl(url)); - 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); - } + @visibleForTesting + factory RampFlutter.uri(Uri widgetUrl) => RampFlutter._(widgetUrl); - void _handleOnSendCryptoRequested(dynamic arguments) { - dynamic payload = arguments[0]; - SendCryptoPayload sendCrypto = SendCryptoPayload.fromArguments(payload); - onSendCryptoRequested!(sendCrypto); - } + RampFlutter._(Uri widgetUrl) : _webView = RampWebView(widgetUrl); - 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); - } + final RampWebView _webView; - void _handleOnRampClosed() { - onRampClosed!(); - } + void Function(WidgetEvent event)? get onWidgetEvent => _webView.onWidgetEvent; - 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; - } + set onWidgetEvent(void Function(WidgetEvent event)? callback) { + _webView.onWidgetEvent = callback; } - Future showRamp( - Configuration configuration, - ) async { - _channel.setMethodCallHandler(_didRecieveMethodCall); - await _channel.invokeMethod('showRamp', configuration.toMap()); - } + Widget get view => _webView.view; - Future sendCrypto(String? transactionHash) async { - await _channel.invokeMethod('sendCrypto', transactionHash); - } + Future postHostEvent(HostEvent event) => _webView.postHostEvent(event); + + void dispose() => _webView.dispose(); + + @visibleForTesting + void handleJavaScriptMessage(String message) => _webView.handleJavaScriptMessage(message); } diff --git a/lib/send_crypto_payload.dart b/lib/send_crypto_payload.dart deleted file mode 100644 index 46f54d9..0000000 --- a/lib/send_crypto_payload.dart +++ /dev/null @@ -1,50 +0,0 @@ -class SendCryptoPayload { - String? address; - String? amount; - SendCryptoAssetInfo? assetInfo; - - static SendCryptoPayload fromArguments(dynamic arguments) { - SendCryptoPayload payload = SendCryptoPayload(); - 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 { - String? chain; - int? decimals; - String? name; - String? symbol; - String? type; - - static SendCryptoAssetInfo fromArguments(dynamic arguments) { - SendCryptoAssetInfo payload = SendCryptoAssetInfo(); - payload.chain = arguments["chain"]; - payload.decimals = arguments["decimals"]; - payload.name = arguments["name"]; - payload.symbol = arguments["symbol"]; - payload.type = arguments["type"]; - return payload; - } - - dynamic toMap() { - return { - 'chain': chain, - 'decimals': decimals, - 'name': name, - 'symbol': symbol, - 'type': type, - }; - } -} diff --git a/lib/src/android_file_selector.dart b/lib/src/android_file_selector.dart new file mode 100644 index 0000000..955c2c3 --- /dev/null +++ b/lib/src/android_file_selector.dart @@ -0,0 +1,74 @@ +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/configuration.dart b/lib/src/configuration.dart new file mode 100644 index 0000000..1b82503 --- /dev/null +++ b/lib/src/configuration.dart @@ -0,0 +1,89 @@ +import 'package:ramp_flutter/src/enums.dart'; +import 'package:ramp_flutter/src/signed_url.dart'; + +export 'package:ramp_flutter/src/enums.dart'; + +class Configuration { + const Configuration({ + this.url, + this.defaultFlow, + this.enabledFlows, + this.enabledCryptoAssets, + this.inAsset, + this.inAssetValue, + this.outAsset, + this.outAssetValue, + this.hostApiKey, + this.offrampWebhookV3Url, + this.paymentMethodType, + this.selectedCountryCode, + 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 int sendCryptoProtocolVersion = 1; + + final String? url; + final TransactionFlow? defaultFlow; + final List? enabledFlows; + final List? enabledCryptoAssets; + final String? inAsset; + final String? inAssetValue; + final String? outAsset; + final String? outAssetValue; + final String? hostApiKey; + final String? offrampWebhookV3Url; + final PaymentMethodType? paymentMethodType; + final String? selectedCountryCode; + final String? userAddress; + final String? userEmailAddress; + final bool? useSendCryptoCallback; + final String? webhookStatusUrl; + + Uri buildWidgetUrl() { + 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.', + ); + } + if (!isTrustedRampWidgetUrl(base)) { + throw ArgumentError.value(base.toString(), 'url', 'Untrusted Ramp Network URL'); + } + + return base.replace( + path: base.path.isEmpty ? '/' : base.path, + 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(hostApiKey)) 'hostApiKey': hostApiKey!, + 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': sendCryptoProtocolVersion.toString(), + 'sdkType': sdkType, + 'sdkVersion': sdkVersion, + }, + ); + } + + static bool _nonEmpty(String? value) => value != null && value.isNotEmpty; +} diff --git a/lib/src/enums.dart b/lib/src/enums.dart new file mode 100644 index 0000000..ab37b73 --- /dev/null +++ b/lib/src/enums.dart @@ -0,0 +1,14 @@ +// ignore_for_file: constant_identifier_names + +enum TransactionFlow { ONRAMP, OFFRAMP, SWAP } + +enum PaymentMethodType { + MANUAL_BANK_TRANSFER, + AUTO_BANK_TRANSFER, + CARD_PAYMENT, + APPLE_PAY, + GOOGLE_PAY, + PIX, + ACH, + PAYPAL, +} diff --git a/lib/src/external_navigation.dart b/lib/src/external_navigation.dart new file mode 100644 index 0000000..c857143 --- /dev/null +++ b/lib/src/external_navigation.dart @@ -0,0 +1,58 @@ +import 'package:flutter/foundation.dart'; +import 'package:url_launcher/url_launcher.dart'; + +bool shouldOpenExternally(Uri uri, Uri widgetUrl) { + final scheme = uri.scheme.toLowerCase(); + 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; + } + if (host == 'www.google.com' || host == 'www.gstatic.com') { + return uri.path.toLowerCase().contains('/recaptcha'); + } + return false; +} + +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/host_event.dart b/lib/src/host_event.dart new file mode 100644 index 0000000..3576f70 --- /dev/null +++ b/lib/src/host_event.dart @@ -0,0 +1,12 @@ +part 'host_events/request_crypto_account_result.dart'; +part 'host_events/send_crypto_result.dart'; + +sealed class HostEvent { + const HostEvent(); + + String get type; + + Map payloadToJson(); + + Map toJson() => {'type': type, 'payload': payloadToJson()}; +} diff --git a/lib/src/host_events/request_crypto_account_result.dart b/lib/src/host_events/request_crypto_account_result.dart new file mode 100644 index 0000000..b28c5df --- /dev/null +++ b/lib/src/host_events/request_crypto_account_result.dart @@ -0,0 +1,52 @@ +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( + RequestCryptoAccountSuccessPayload(address: address, type: type, name: name, assetSymbol: assetSymbol), + ); + + factory RequestCryptoAccountResult.error([String? error]) => + RequestCryptoAccountResult(RequestCryptoAccountErrorPayload(error)); + + final RequestCryptoAccountResultPayload payload; + + @override + String get type => 'REQUEST_CRYPTO_ACCOUNT_RESULT'; + + @override + 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/src/host_events/send_crypto_result.dart b/lib/src/host_events/send_crypto_result.dart new file mode 100644 index 0000000..7cd6f11 --- /dev/null +++ b/lib/src/host_events/send_crypto_result.dart @@ -0,0 +1,41 @@ +part of '../host_event.dart'; + +final class SendCryptoResult extends HostEvent { + const SendCryptoResult(this.payload); + + factory SendCryptoResult.txHash(String? txHash) => SendCryptoResult(SendCryptoResultTxHashPayload(txHash)); + + factory SendCryptoResult.error([String? error]) => SendCryptoResult(SendCryptoResultErrorPayload(error)); + + final SendCryptoResultPayload payload; + + @override + String get type => 'SEND_CRYPTO_RESULT'; + + @override + 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/src/models/asset_info.dart b/lib/src/models/asset_info.dart new file mode 100644 index 0000000..8a4cf99 --- /dev/null +++ b/lib/src/models/asset_info.dart @@ -0,0 +1,28 @@ +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}); + + 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/src/models/json_map.dart b/lib/src/models/json_map.dart new file mode 100644 index 0000000..965646c --- /dev/null +++ b/lib/src/models/json_map.dart @@ -0,0 +1,46 @@ +Map? asStringKeyedMap(dynamic value) { + if (value is Map) { + return value; + } + if (value is Map) { + return Map.from(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/src/models/purchase_details.dart b/lib/src/models/purchase_details.dart new file mode 100644 index 0000000..3520b9a --- /dev/null +++ b/lib/src/models/purchase_details.dart @@ -0,0 +1,76 @@ +import 'package:ramp_flutter/src/models/asset_info.dart'; +import 'package:ramp_flutter/src/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/src/models/sale_details.dart b/lib/src/models/sale_details.dart new file mode 100644 index 0000000..500d7ea --- /dev/null +++ b/lib/src/models/sale_details.dart @@ -0,0 +1,83 @@ +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}); + + 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/src/ramp_webview.dart b/lib/src/ramp_webview.dart new file mode 100644 index 0000000..e9523a4 --- /dev/null +++ b/lib/src/ramp_webview.dart @@ -0,0 +1,190 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/widgets.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:webview_flutter/webview_flutter.dart'; +import 'package:webview_flutter_android/webview_flutter_android.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; + +class RampWebView { + RampWebView(this._widgetUrl); + + static const _channelName = 'RampInstantMobile'; + + final Uri _widgetUrl; + WebViewController? _controller; + var _pageReady = false; + final _pendingHostEvents = <({HostEvent event, Completer done})>[]; + + void Function(WidgetEvent event)? onWidgetEvent; + + Widget get view => WebViewWidget(controller: _ensureController()); + + 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), + ); + + _configureAndroid(controller); + controller.loadRequest(_widgetUrl); + return controller; + } + + PlatformWebViewControllerCreationParams _platformParams() { + if (WebViewPlatform.instance is WebKitWebViewPlatform) { + return WebKitWebViewControllerCreationParams(allowsInlineMediaPlayback: true); + } + return const PlatformWebViewControllerCreationParams(); + } + + NavigationDelegate _navigationDelegate() { + return NavigationDelegate( + onNavigationRequest: _onNavigationRequest, + onCreateWindow: _onCreateWindow, + onPageStarted: (_) => _pageReady = false, + onPageFinished: (_) { + _pageReady = true; + _flushPendingHostEvents(); + }, + onWebResourceError: _onWebResourceError, + onHttpError: _onHttpError, + ); + } + + 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(); + } + + 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) { + _ensureController(); + if (_pageReady) { + return _sendHostEvent(event); + } + 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()); + 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')); + _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); + } +} diff --git a/lib/src/signed_url.dart b/lib/src/signed_url.dart new file mode 100644 index 0000000..fea808c --- /dev/null +++ b/lib/src/signed_url.dart @@ -0,0 +1,17 @@ +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 || + !isTrustedRampWidgetUrl(url) || + !requiredParameters.every((parameter) => parameters[parameter]?.isNotEmpty == true)) { + throw ArgumentError.value(value, 'url', 'Invalid signed Ramp Network URL'); + } + return url; +} diff --git a/lib/src/widget_event.dart b/lib/src/widget_event.dart new file mode 100644 index 0000000..583199f --- /dev/null +++ b/lib/src/widget_event.dart @@ -0,0 +1,82 @@ +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'; +import 'package:ramp_flutter/src/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 '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'; +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}); + + final String? widgetInstanceId; + + 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']); + final widgetInstanceId = json['widgetInstanceId'] as String?; + + 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': + return WidgetConfigFailed(widgetInstanceId: widgetInstanceId); + case 'PURCHASE_CREATED': + 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 != Configuration.sendCryptoProtocolVersion) { + debugPrint('RampFlutter: drop SEND_CRYPTO — unsupported eventVersion=$version'); + return null; + } + if (payload == null) { + debugPrint('RampFlutter: drop SEND_CRYPTO — missing payload'); + return null; + } + return SendCryptoRequested(SendCryptoPayload.fromJson(payload), widgetInstanceId: widgetInstanceId); + case 'REQUEST_CRYPTO_ACCOUNT': + if (payload == null) { + debugPrint('RampFlutter: drop REQUEST_CRYPTO_ACCOUNT — missing payload'); + return null; + } + return RequestCryptoAccount( + RequestCryptoAccountPayload.fromJson(payload), + widgetInstanceId: widgetInstanceId, + ); + case 'WIDGET_CLOSE': + case 'CLOSE': + return WidgetClose(payload: WidgetClosePayload.fromJson(payload), widgetInstanceId: widgetInstanceId); + case 'WIDGET_CLOSE_REQUEST': + return WidgetCloseRequest(widgetInstanceId: widgetInstanceId); + default: + debugPrint('RampFlutter: drop widget event — unknown type=$type'); + return null; + } + } catch (error, stackTrace) { + debugPrint('RampFlutter: drop widget event type=$type — parse error: $error\n$stackTrace'); + return null; + } + } +} 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/lib/src/widget_events/offramp_sale_created.dart b/lib/src/widget_events/offramp_sale_created.dart new file mode 100644 index 0000000..293284a --- /dev/null +++ b/lib/src/widget_events/offramp_sale_created.dart @@ -0,0 +1,26 @@ +part of '../widget_event.dart'; + +final class OfframpSaleCreated extends WidgetEvent { + 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?, + ); + } +} diff --git a/lib/src/widget_events/purchase_created.dart b/lib/src/widget_events/purchase_created.dart new file mode 100644 index 0000000..24aae98 --- /dev/null +++ b/lib/src/widget_events/purchase_created.dart @@ -0,0 +1,26 @@ +part of '../widget_event.dart'; + +final class PurchaseCreated extends WidgetEvent { + 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?, + ); + } +} diff --git a/lib/src/widget_events/request_crypto_account.dart b/lib/src/widget_events/request_crypto_account.dart new file mode 100644 index 0000000..ffc09a3 --- /dev/null +++ b/lib/src/widget_events/request_crypto_account.dart @@ -0,0 +1,21 @@ +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}); + + 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/src/widget_events/send_crypto_requested.dart b/lib/src/widget_events/send_crypto_requested.dart new file mode 100644 index 0000000..19e2cb9 --- /dev/null +++ b/lib/src/widget_events/send_crypto_requested.dart @@ -0,0 +1,26 @@ +part of '../widget_event.dart'; + +final class SendCryptoRequested extends WidgetEvent { + const SendCryptoRequested(this.payload, {super.widgetInstanceId}); + + final SendCryptoPayload payload; +} + +class SendCryptoPayload { + const SendCryptoPayload({this.address, this.amount, this.assetInfo}); + + final String? address; + final String? amount; + final AssetInfo? assetInfo; + + factory SendCryptoPayload.fromJson(Map? json) { + if (json == null) { + return const SendCryptoPayload(); + } + return SendCryptoPayload( + address: json['address'] as String?, + amount: asString(json['amount']), + assetInfo: AssetInfo.fromJson(asStringKeyedMap(json['assetInfo'])), + ); + } +} diff --git a/lib/src/widget_events/widget_close.dart b/lib/src/widget_events/widget_close.dart new file mode 100644 index 0000000..cfc5f23 --- /dev/null +++ b/lib/src/widget_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/src/widget_events/widget_close_request.dart b/lib/src/widget_events/widget_close_request.dart new file mode 100644 index 0000000..670d523 --- /dev/null +++ b/lib/src/widget_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/src/widget_events/widget_config_done.dart b/lib/src/widget_events/widget_config_done.dart new file mode 100644 index 0000000..c35ee60 --- /dev/null +++ b/lib/src/widget_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/src/widget_events/widget_config_failed.dart b/lib/src/widget_events/widget_config_failed.dart new file mode 100644 index 0000000..9afb0aa --- /dev/null +++ b/lib/src/widget_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/pubspec.lock b/pubspec.lock deleted file mode 100644 index d512e20..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,80 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - characters: - dependency: transitive - description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - collection: - dependency: transitive - description: - name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.dev" - source: hosted - version: "1.18.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 - url: "https://pub.dev" - source: hosted - version: "2.0.3" - lints: - dependency: transitive - description: - name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" - url: "https://pub.dev" - source: hosted - version: "0.8.0" - meta: - dependency: transitive - description: - name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" - url: "https://pub.dev" - source: hosted - version: "1.12.0" - plugin_platform_interface: - dependency: "direct main" - 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.99" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" - source: hosted - version: "2.1.4" -sdks: - dart: ">=3.3.0-0 <4.0.0" - flutter: ">=3.3.0" diff --git a/pubspec.yaml b/pubspec.yaml index 49c642d..e289c52 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,25 +1,38 @@ 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: - sdk: '>=3.2.3 <4.0.0' - flutter: '>=3.3.0' + sdk: ^3.12.2 + flutter: ">=3.44.0" dependencies: flutter: sdk: flutter - plugin_platform_interface: ^2.0.2 + file_picker: ^10.1.2 + image_picker: ^1.1.2 + url_launcher: ^6.3.2 + # 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: a37d157a9160fbd509484e9bb176be038c4d153c + path: packages/webview_flutter/webview_flutter + webview_flutter_android: + git: + url: https://github.com/mateusz-ramp/flutter_packages.git + ref: a37d157a9160fbd509484e9bb176be038c4d153c + path: packages/webview_flutter/webview_flutter_android + webview_flutter_wkwebview: + git: + url: https://github.com/mateusz-ramp/flutter_packages.git + ref: a37d157a9160fbd509484e9bb176be038c4d153c + path: packages/webview_flutter/webview_flutter_wkwebview dev_dependencies: - flutter_lints: ^2.0.0 - -flutter: - plugin: - platforms: - android: - package: network.ramp.ramp_flutter - pluginClass: RampFlutterPlugin - ios: - pluginClass: RampFlutterPlugin + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 diff --git a/ramp_flutter.iml b/ramp_flutter.iml deleted file mode 100644 index 27686dd..0000000 --- a/ramp_flutter.iml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - 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 new file mode 100644 index 0000000..84b5006 --- /dev/null +++ b/test/ramp_webview_test.dart @@ -0,0 +1,263 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:ramp_flutter/ramp_flutter.dart'; + +void main() { + group('Configuration.buildWidgetUrl', () { + test('uses default base URL and SDK metadata', () { + final url = const 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.containsKey('variant'), isFalse); + }); + + test('merges configuration fields and joins list params', () { + final url = const Configuration( + url: 'https://app.dev.ramp-network.org/custom', + hostApiKey: 'key', + enabledCryptoAssets: ['ETH_*', 'BTC_BTC'], + inAsset: 'EUR', + outAsset: 'ETH_ETH', + inAssetValue: '10000', + outAssetValue: '500000', + 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, + useSendCryptoCallback: true, + ).buildWidgetUrl(); + + expect(url.host, 'app.dev.ramp-network.org'); + 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['outAssetValue'], '500000'); + 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, + 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', () { + expect(() => const Configuration(url: 'https://evil.example/').buildWidgetUrl(), throwsArgumentError); + expect(() => const Configuration(url: 'http://app.rampnetwork.com/').buildWidgetUrl(), throwsArgumentError); + }); + }); + + group('RampFlutter event parsing', () { + final widgetUrl = Uri.parse('https://app.rampnetwork.com/'); + + test('ignores non-JSON and unknown types', () { + final events = []; + final ramp = RampFlutter.uri(widgetUrl)..onWidgetEvent = events.add; + + ramp.handleJavaScriptMessage('not json'); + ramp.handleJavaScriptMessage('42'); + ramp.handleJavaScriptMessage(jsonEncode({'type': 'SHARE_LINK'})); + + expect(events, isEmpty); + }); + + test('parses supported widget events', () { + 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'}), + ); + ramp.handleJavaScriptMessage(jsonEncode({'type': 'WIDGET_CONFIG_FAILED', 'payload': null})); + ramp.handleJavaScriptMessage( + jsonEncode({ + 'type': 'PURCHASE_CREATED', + 'payload': { + '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', + }, + }), + ); + ramp.handleJavaScriptMessage( + jsonEncode({ + 'type': 'OFFRAMP_SALE_CREATED', + 'payload': { + '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', + }, + }), + ); + ramp.handleJavaScriptMessage( + jsonEncode({ + 'type': 'SEND_CRYPTO', + 'eventVersion': 1, + 'payload': { + 'address': '0xabc', + 'amount': '1', + '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_REQUEST', 'payload': null})); + + expect(events, [ + isA(), + isA(), + isA(), + isA(), + isA(), + isA(), + isA(), + isA(), + isA(), + isA(), + ]); + + final appVersion = events[0] as AppVersion; + expect(appVersion.widgetInstanceId, 'w0'); + expect(appVersion.payload.version, '2.0'); + expect(events[1].widgetInstanceId, 'w1'); + + 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[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[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[6] as RequestCryptoAccount; + expect(account.payload.type, 'ETH'); + expect(account.payload.assetSymbol, 'ETH'); + + final close = events[7] as WidgetClose; + expect(close.payload.showAlert, isTrue); + expect(close.payload.descriptionText, 'Leave?'); + }); + + test('rejects unsupported SEND_CRYPTO eventVersion', () { + final events = []; + final ramp = RampFlutter.uri(widgetUrl)..onWidgetEvent = events.add; + + ramp.handleJavaScriptMessage( + jsonEncode({ + 'type': 'SEND_CRYPTO', + 'eventVersion': 2, + 'payload': {'address': '0xabc', 'amount': '1', 'assetInfo': {}}, + }), + ); + + 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(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'}, + }); + expect(RequestCryptoAccountResult.error('denied').toJson(), { + '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'); + }); + }); +} diff --git a/test/signed_url_test.dart b/test/signed_url_test.dart new file mode 100644 index 0000000..47d8197 --- /dev/null +++ b/test/signed_url_test.dart @@ -0,0 +1,46 @@ +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); + }); +}