Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ This plugin requires several changes to be able to work on Android devices. Plea

8. Rebuild the app, as the above changes don't update with hot reload

> **`EmbeddedPaymentElement`**: this widget depends on Jetpack Compose. The
> `stripe_android` package brings its own `compose-compiler-gradle-plugin`
> pinned to Kotlin 2.1.0. If your host app uses a different Kotlin version you
> may need to align it, or apply the compose-compiler plugin yourself. See the
> [`stripe_android` README](https://github.com/flutter-stripe/flutter_stripe/tree/main/packages/stripe_android#kotlin--compose-requirement).

These changes are needed because the Android Stripe SDK requires the use of the AppCompat theme for their UI components and the Support Fragment Manager for the Payment Sheets

If you are having troubles to make this package to work on Android, join [this discussion](https://github.com/flutter-stripe/flutter_stripe/discussions/538) to get some support.
Expand Down
6 changes: 6 additions & 0 deletions packages/stripe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ This plugin requires several changes to be able to work on Android devices. Plea

8. Rebuild the app, as the above changes don't update with hot reload

> **`EmbeddedPaymentElement`**: this widget depends on Jetpack Compose. The
> `stripe_android` package brings its own `compose-compiler-gradle-plugin`
> pinned to Kotlin 2.1.0. If your host app uses a different Kotlin version you
> may need to align it, or apply the compose-compiler plugin yourself. See the
> [`stripe_android` README](https://github.com/flutter-stripe/flutter_stripe/tree/main/packages/stripe_android#kotlin--compose-requirement).

These changes are needed because the Android Stripe SDK requires the use of the AppCompat theme for their UI components and the Support Fragment Manager for the Payment Sheets

If you are having troubles to make this package to work on Android, join [this discussion](https://github.com/flutter-stripe/flutter_stripe/discussions/538) to get some support.
Expand Down
54 changes: 49 additions & 5 deletions packages/stripe/lib/src/widgets/embedded_payment_element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ class EmbeddedPaymentElementLoadingException implements Exception {
/// Allows users to select and configure payment methods inline within your app.
/// Supports saved payment methods, new cards, Apple Pay, Google Pay, and more.
///
/// Only supported on iOS and Android platforms.
/// Throws [UnsupportedError] on web and desktop — only iOS and Android are
/// supported.
class EmbeddedPaymentElement extends StatefulWidget {
/// Creates an embedded payment element.
const EmbeddedPaymentElement({
Expand All @@ -65,6 +66,7 @@ class EmbeddedPaymentElement extends StatefulWidget {
this.onLoadingFailed,
this.onFormSheetConfirmComplete,
this.onRowSelectionImmediateAction,
this.loadingBuilder,
super.key,
this.androidPlatformViewRenderType =
AndroidPlatformViewRenderType.expensiveAndroidView,
Expand Down Expand Up @@ -94,6 +96,15 @@ class EmbeddedPaymentElement extends StatefulWidget {
/// Called when row selection triggers immediate action.
final RowSelectionImmediateActionCallback? onRowSelectionImmediateAction;

/// Builds the widget shown while the element is loading, before it reports
/// its first measured height.
///
/// Use this to match your app's loading conventions (a branded spinner, a
/// shimmer/skeleton, or nothing at all). The returned widget sizes the
/// loading area, so it is not clipped to a fixed height. When null, a
/// centered [CircularProgressIndicator] is shown.
final WidgetBuilder? loadingBuilder;

/// Android platform view rendering mode.
final AndroidPlatformViewRenderType androidPlatformViewRenderType;

Expand Down Expand Up @@ -387,14 +398,24 @@ class _EmbeddedPaymentElementState extends State<EmbeddedPaymentElement>
);
}

final isLoading = _currentHeight == 0;

return AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: SizedBox(
height: _currentHeight > 0 ? _currentHeight : 400,
child: platformView,
),
child: isLoading
? Stack(
children: [
// Keep the platform view mounted so it can initialize and
// report its height. The loading widget is the only
// non-positioned child, so it sizes the Stack.
Positioned.fill(child: platformView),
widget.loadingBuilder?.call(context) ??
const _DefaultEmbeddedLoading(),
],
)
: SizedBox(height: _currentHeight, child: platformView),
);
}

Expand Down Expand Up @@ -520,3 +541,26 @@ class _UiKitEmbeddedPaymentElement extends StatelessWidget {
);
}
}

/// Default loading indicator shown until the element reports its height.
///
/// [Center.heightFactor] keeps the widget's height bounded to its child, so it
/// can size the loading area even when the surrounding constraints are
/// unbounded (e.g. inside a scroll view).
class _DefaultEmbeddedLoading extends StatelessWidget {
const _DefaultEmbeddedLoading();

@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 24),
child: Center(
heightFactor: 1,
child: SizedBox.square(
dimension: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
);
}
}
7 changes: 7 additions & 0 deletions packages/stripe_android/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

This is an implementation of the [`stripe_platform_interface`](https://github.com/flutter-stripe/flutter_stripe/tree/main/packages/stripe_platform_interface) package for Android.

### Kotlin / Compose requirement

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be added to the main readme as well


`EmbeddedPaymentElement` depends on Jetpack Compose. This package brings its
own `compose-compiler-gradle-plugin` pinned to Kotlin 2.1.0. Host apps using a
different Kotlin version may need to align their host-app Kotlin version or
apply the compose-compiler plugin themselves.

## Usage

### With the `flutter_stripe` plugin
Expand Down
9 changes: 1 addition & 8 deletions packages/stripe_android/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ rootProject.allprojects {
}

apply plugin: 'com.android.library'
// Required by EmbeddedPaymentElement; pairs with kotlin_version above.
apply plugin: "org.jetbrains.kotlin.plugin.compose"
apply plugin: 'kotlin-android'

Expand All @@ -41,14 +42,6 @@ android {
jvmTarget = '17'
}

buildFeatures {
compose true
}

composeOptions {
kotlinCompilerExtensionVersion = '1.5.1'
}

sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:stripe_platform_interface/stripe_platform_interface.dart';

void main() {
group('IntentConfiguration wire contract', () {
test('paymentMode serializes expected keys', () {
final config = IntentConfiguration(
mode: const IntentMode.paymentMode(currencyCode: 'USD', amount: 1500),
paymentMethodConfigurationId: 'pmc_123',
);
final json = config.toJson();
expect(json['mode'], isA<Map>());
expect((json['mode'] as Map)['currencyCode'], 'USD');
expect((json['mode'] as Map)['amount'], 1500);
expect(json['paymentMethodConfigurationId'], 'pmc_123');
// confirmHandler must NOT be serialized from Dart side; the native side
// adds the `true` sentinel that triggers the reverse callback.
expect(json.containsKey('confirmHandler'), isFalse);
expect(json.containsKey('confirmTokenHandler'), isFalse);
expect(json.containsKey('confirmationTokenConfirmHandler'), isFalse);
});

test('setupMode serializes expected keys', () {
final config = IntentConfiguration(
mode: const IntentMode.setupMode(
currencyCode: 'USD',
setupFutureUsage: IntentFutureUsage.OffSession,
),
);
final json = config.toJson();
expect(json['mode'], isA<Map>());
expect((json['mode'] as Map)['setupFutureUsage'], 'OffSession');
});
});

group('SetupPaymentSheetParameters wire contract', () {
test('appearance.embeddedPaymentElement.row serializes', () {
const params = SetupPaymentSheetParameters(
merchantDisplayName: 'Test',
appearance: PaymentSheetAppearance(
embeddedPaymentElement: EmbeddedPaymentElementAppearance(
row: RowConfig(additionalInsets: 8),
),
),
);
final json = params.toJson();
final appearance = json['appearance'] as Map<String, dynamic>;
final embedded =
appearance['embeddedPaymentElement'] as Map<String, dynamic>;
final row = embedded['row'] as Map<String, dynamic>;
expect(row['additionalInsets'], 8);
});
});
}
Loading