diff --git a/CHANGELOG.md b/CHANGELOG.md index c95490b..da87407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 2.2.8 +* Fix recommended size is shown in InPage even when there is no available size in the comparison screen +* Fix: Virtusize button state when loading a new product +* Fix: Android: InPage Standard sometimes does not appear +* Fix: Android: VS widget sometimes does not show when navigating between different colors +* Update native SDK support to Android 2.12.15 & iOS 2.12.22 + ## 2.2.7 * Fix: the human icon thumbnail is missing in inpage * Fix: opening the privacy policy redirects to an external browser diff --git a/android/build.gradle b/android/build.gradle index 807febf..07fe669 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,7 +7,7 @@ buildscript { ext.cardview_version = "1.0.0" ext.coroutines_version = "1.5.0" ext.gradle_version = "8.7.0" - ext.virtusize_version = "2.12.13" + ext.virtusize_version = "2.12.15" repositories { google() diff --git a/android/src/main/kotlin/com/virtusize/virtusize_flutter_sdk/VirtusizeFlutterPlugin.kt b/android/src/main/kotlin/com/virtusize/virtusize_flutter_sdk/VirtusizeFlutterPlugin.kt index 62e2fea..283e8f3 100644 --- a/android/src/main/kotlin/com/virtusize/virtusize_flutter_sdk/VirtusizeFlutterPlugin.kt +++ b/android/src/main/kotlin/com/virtusize/virtusize_flutter_sdk/VirtusizeFlutterPlugin.kt @@ -30,13 +30,18 @@ class VirtusizeFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { private var virtusizeFlutterPresenter: VirtusizeFlutterPresenter = object : VirtusizeFlutterPresenter { override fun onValidProductCheck(productWithPCDData: VirtusizeProduct) { - channel.invokeMethod( - VirtusizeFlutterMethod.ON_PRODUCT_DATA_CHECK, - mutableMapOf( - VirtusizeFlutterKey.EXTERNAL_PRODUCT_ID to productWithPCDData.externalId, - VirtusizeFlutterKey.IS_VALID_PRODUCT to (productWithPCDData.productCheckData?.data?.validProduct ?: false), + // Post to main thread with delay to ensure Flutter widgets have time to subscribe + // This handles the case where cached data causes synchronous callback execution + scope.launch { + kotlinx.coroutines.delay(100) + channel.invokeMethod( + VirtusizeFlutterMethod.ON_PRODUCT_DATA_CHECK, + mutableMapOf( + VirtusizeFlutterKey.EXTERNAL_PRODUCT_ID to productWithPCDData.externalId, + VirtusizeFlutterKey.IS_VALID_PRODUCT to (productWithPCDData.productCheckData?.data?.validProduct ?: false), + ) ) - ) + } } override fun hasInPageError( @@ -54,18 +59,27 @@ class VirtusizeFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { storeProduct: Product?, bestUserProduct: Product?, recommendationText: String?, + willFit: Boolean?, ) { - sendOnProductResult(storeProduct, "store") - sendOnProductResult(bestUserProduct, "user") + scope.launch { + kotlinx.coroutines.delay(100) + sendOnProductResult(storeProduct, "store") + sendOnProductResult(bestUserProduct, "user") - channel.invokeMethod( - VirtusizeFlutterMethod.ON_REC_CHANGE, - mutableMapOf( - VirtusizeFlutterKey.EXTERNAL_PRODUCT_ID to externalProductId, - VirtusizeFlutterKey.REC_TEXT to recommendationText, - VirtusizeFlutterKey.SHOW_USER_PRODUCT_IMAGE to true + // Show user product image only if: + // - willFit is not explicitly false (when false, it means "Your size not found") + // - AND there is a bestUserProduct available + val showUserProductImage = willFit != false && bestUserProduct != null + + channel.invokeMethod( + VirtusizeFlutterMethod.ON_REC_CHANGE, + mutableMapOf( + VirtusizeFlutterKey.EXTERNAL_PRODUCT_ID to externalProductId, + VirtusizeFlutterKey.REC_TEXT to recommendationText, + VirtusizeFlutterKey.SHOW_USER_PRODUCT_IMAGE to showUserProductImage + ) ) - ) + } } override fun onLangugeClick(language: VirtusizeLanguage) { @@ -210,6 +224,7 @@ class VirtusizeFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { return } virtusizeFlutter.setUserId(call.arguments.toString()) + result.success(true) } VirtusizeFlutterMethod.LOAD_VIRTUSIZE -> { val externalId = call.argument(VirtusizeFlutterKey.EXTERNAL_PRODUCT_ID) @@ -224,6 +239,7 @@ class VirtusizeFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { ) virtusizeFlutter.load(virtusizeProduct) + result.success(true) } VirtusizeFlutterMethod.OPEN_VIRTUSIZE_WEB_VIEW -> { val externalProductId = call.arguments as? String @@ -237,6 +253,7 @@ class VirtusizeFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { activity, externalProductId ) + result.success(true) } VirtusizeFlutterMethod.GET_PRIVACY_POLICY_LINK -> { result.success( diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index afa1e8e..efdcc4a 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-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/example/android/settings.gradle.kts b/example/android/settings.gradle.kts index a439442..f515db5 100644 --- a/example/android/settings.gradle.kts +++ b/example/android/settings.gradle.kts @@ -18,7 +18,7 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.7.0" apply false + id("com.android.application") version "8.9.1" apply false id("org.jetbrains.kotlin.android") version "1.8.22" apply false } diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift index 961e500..3cbea62 100644 --- a/example/ios/RunnerTests/RunnerTests.swift +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -73,13 +73,14 @@ class RunnerTests: XCTestCase { VirtusizeFlutterKey.imageURL: "https://example.com/image.jpg" ] ) - + var resultData: Any? plugin.handle(call) { result in resultData = result } - - XCTAssertNil(resultData) + + XCTAssertNotNil(resultData) + XCTAssertEqual(resultData as? Bool, true) } func testLoadVirtusizeWithoutProductId() { @@ -106,13 +107,14 @@ class RunnerTests: XCTestCase { methodName: VirtusizeFlutterMethod.setUserId, arguments: "test_user_id" ) - + var resultData: Any? plugin.handle(call) { result in resultData = result } - - XCTAssertNil(resultData) + + XCTAssertNotNil(resultData) + XCTAssertEqual(resultData as? Bool, true) } func testSetUserIdWithEmptyId() { @@ -139,13 +141,14 @@ class RunnerTests: XCTestCase { methodName: VirtusizeFlutterMethod.openVirtusizeWebView, arguments: "product123" ) - + var resultData: Any? plugin.handle(call) { result in resultData = result } - - XCTAssertNil(resultData) + + XCTAssertNotNil(resultData) + XCTAssertEqual(resultData as? Bool, true) } func testOpenVirtusizeWebViewWithoutProductId() { diff --git a/example/lib/screens/home_screen.dart b/example/lib/screens/home_screen.dart index b37fa1d..e0cf8b9 100644 --- a/example/lib/screens/home_screen.dart +++ b/example/lib/screens/home_screen.dart @@ -121,7 +121,6 @@ class _HomeScreenState extends State { VirtusizeInPageMini( product: _product, backgroundColor: Colors.blue, - horizontalMargin: 32, ), Container(height: 16), @@ -136,7 +135,6 @@ class _HomeScreenState extends State { VirtusizeInPageStandard( product: _product, buttonBackgroundColor: Colors.amber, - horizontalMargin: 32, ), Container(height: 16), diff --git a/ios/Classes/SwiftVirtusizeFlutterPlugin.swift b/ios/Classes/SwiftVirtusizeFlutterPlugin.swift index 5d5387b..8b4d15c 100644 --- a/ios/Classes/SwiftVirtusizeFlutterPlugin.swift +++ b/ios/Classes/SwiftVirtusizeFlutterPlugin.swift @@ -128,18 +128,20 @@ public class SwiftVirtusizeFlutterPlugin: NSObject, FlutterPlugin { if let imageURLString = arguments[VirtusizeFlutterKey.imageURL] as? String { imageURL = URL(string: imageURLString) } - + let product = VirtusizeProduct(externalId: externalProductId, imageURL: imageURL) VirtusizeFlutter.load(product: product) + result(true) case VirtusizeFlutterMethod.openVirtusizeWebView: guard let externalProductId = call.arguments as? String else { result(FlutterError.noArguments) return } - + VirtusizeFlutter.openVirtusizeWebView( externalId: externalProductId, messageHandler: self) + result(true) case VirtusizeFlutterMethod.getPrivacyPolicyLink: result(VirtusizeFlutter.getPrivacyPolicyLink()) case VirtusizeFlutterMethod.sendOrder: @@ -180,7 +182,8 @@ extension SwiftVirtusizeFlutterPlugin: VirtusizeFlutterProductEventHandler { clientProductImageURL: String?, storeProduct: VirtusizeServerProduct, bestUserProduct: VirtusizeServerProduct?, - recommendationText: String) { + recommendationText: String, + willFit: Bool?) { DispatchQueue.main.async { [self] in self.flutterChannel?.invokeMethod( VirtusizeFlutterMethod.onProduct, @@ -193,7 +196,7 @@ extension SwiftVirtusizeFlutterPlugin: VirtusizeFlutterProductEventHandler { VirtusizeFlutterKey.productStyle: storeProduct.productStyle ] ) - + self.flutterChannel?.invokeMethod( VirtusizeFlutterMethod.onProduct, arguments: [ @@ -204,13 +207,18 @@ extension SwiftVirtusizeFlutterPlugin: VirtusizeFlutterProductEventHandler { VirtusizeFlutterKey.productStyle: bestUserProduct?.productStyle ] ) - + + // Show user product image only if: + // - willFit is not explicitly false (when false, it means "Your size not found") + // - AND there is a bestUserProduct available + let showUserProductImage = willFit != false && bestUserProduct != nil + self.flutterChannel?.invokeMethod( VirtusizeFlutterMethod.onRecChange, arguments: [ VirtusizeFlutterKey.externalProductId: externalId, VirtusizeFlutterKey.recText: recommendationText, - VirtusizeFlutterKey.showUserProductImage: true + VirtusizeFlutterKey.showUserProductImage: showUserProductImage ] ) } diff --git a/ios/virtusize_flutter_sdk.podspec b/ios/virtusize_flutter_sdk.podspec index f586965..90442fd 100644 --- a/ios/virtusize_flutter_sdk.podspec +++ b/ios/virtusize_flutter_sdk.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'virtusize_flutter_sdk' - s.version = '2.2.7' + s.version = '2.2.8' s.summary = 'Virtusize SDK for Flutter.' s.description = <<-DESC This SDK helps clients to integrate Virtusize’s size and fit service into their Flutter applications for Android & iOS. @@ -17,7 +17,7 @@ This SDK helps clients to integrate Virtusize’s size and fit service into thei s.resources = 'Resources/**/*.json' s.resource_bundle = { 'virtusize_flutter_sdk' => ['Resources/**/*.json'] } s.dependency 'Flutter' - s.dependency 'Virtusize', '~> 2.12.19' + s.dependency 'Virtusize', '~> 2.12.22' s.static_framework = true s.platform = :ios, '14.0' diff --git a/lib/src/widgets/virtusize_button.dart b/lib/src/widgets/virtusize_button.dart index b109971..8abccd9 100644 --- a/lib/src/widgets/virtusize_button.dart +++ b/lib/src/widgets/virtusize_button.dart @@ -14,18 +14,15 @@ class VirtusizeButton extends StatefulWidget { final Widget? child; final VirtusizeStyle style; - const VirtusizeButton({ - super.key, - required this.product, - required Widget this.child, - }) : style = VirtusizeStyle.none; + VirtusizeButton({required this.product, required Widget this.child}) + : style = VirtusizeStyle.none, + super(key: ValueKey('button_${product.externalProductId}')); - const VirtusizeButton.vsStyle({ - super.key, + VirtusizeButton.vsStyle({ required this.product, this.style = VirtusizeStyle.black, this.child, - }); + }) : super(key: ValueKey('vs_button_${product.externalProductId}')); @override // ignore: library_private_types_in_public_api @@ -38,6 +35,8 @@ class _VirtusizeButtonState extends State { VSText _vsText = IVirtusizeSDK.instance.vsText; bool _isValidProduct = false; + Timer? _productDataCheckTimeout; + bool _productDataCheckTimedOut = false; @override void initState() { @@ -56,22 +55,54 @@ class _VirtusizeButtonState extends State { productDataCheck.externalProductId) { return; } + _productDataCheckTimeout?.cancel(); setState(() { _isValidProduct = productDataCheck.isValidProduct; + _productDataCheckTimedOut = false; }); }); + + // Start timeout timer for product data check + _startProductDataCheckTimeout(); + } + + void _startProductDataCheckTimeout() { + _productDataCheckTimeout?.cancel(); + _productDataCheckTimedOut = false; + + _productDataCheckTimeout = Timer(Duration(seconds: 10), () { + if (!mounted) return; + if (!_isValidProduct) { + setState(() { + _productDataCheckTimedOut = true; + }); + } + }); + } + + @override + void didUpdateWidget(VirtusizeButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.product.externalProductId != widget.product.externalProductId) { + setState(() { + _isValidProduct = false; + }); + _startProductDataCheckTimeout(); + } } @override void dispose() { _vsTextSubscription.cancel(); _pdcSubscription.cancel(); + _productDataCheckTimeout?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { - if (_isValidProduct == true) { + // Show the button only when the product is confirmed valid + if (_isValidProduct) { switch (widget.style) { case VirtusizeStyle.none: return widget.child!; diff --git a/lib/src/widgets/virtusize_inpage_standard.dart b/lib/src/widgets/virtusize_inpage_standard.dart index 0645611..e4eaf51 100644 --- a/lib/src/widgets/virtusize_inpage_standard.dart +++ b/lib/src/widgets/virtusize_inpage_standard.dart @@ -25,21 +25,21 @@ class VirtusizeInPageStandard extends StatefulWidget { final EdgeInsets margin; final bool alwaysShowUserProductImage; - const VirtusizeInPageStandard({ - super.key, + VirtusizeInPageStandard({ required this.product, this.buttonBackgroundColor = VSColors.vsGray900, this.margin = const EdgeInsets.symmetric(horizontal: 16), this.alwaysShowUserProductImage = true, - }) : style = VirtusizeStyle.none; + }) : style = VirtusizeStyle.none, + super(key: ValueKey('standard_${product.externalProductId}')); - const VirtusizeInPageStandard.vsStyle({ - super.key, + VirtusizeInPageStandard.vsStyle({ required this.product, this.style = VirtusizeStyle.black, this.margin = const EdgeInsets.symmetric(horizontal: 16), this.alwaysShowUserProductImage = true, - }) : buttonBackgroundColor = VSColors.vsGray900; + }) : buttonBackgroundColor = VSColors.vsGray900, + super(key: ValueKey('vs_standard_${product.externalProductId}')); @override // ignore: library_private_types_in_public_api @@ -64,6 +64,8 @@ class _VirtusizeInPageStandardState extends State { String? _topRecText; String? _bottomRecText; late bool _showPrivacyPolicy; + Timer? _productDataCheckTimeout; + bool _productDataCheckTimedOut = false; @override void initState() { @@ -79,10 +81,12 @@ class _VirtusizeInPageStandardState extends State { if (widget.product.externalProductId != pdc.externalProductId) { return; } + _productDataCheckTimeout?.cancel(); setState(() { _isLoading = true; _hasError = false; _productDataCheck = pdc; + _productDataCheckTimedOut = false; }); }); @@ -119,9 +123,10 @@ class _VirtusizeInPageStandardState extends State { return; } setState(() { - _showUserProductImage = widget.alwaysShowUserProductImage - ? true - : recommendation.showUserProductImage; + _showUserProductImage = + widget.alwaysShowUserProductImage + ? true + : recommendation.showUserProductImage; try { _splitRecTexts(recommendation.text); } catch (e) { @@ -142,6 +147,42 @@ class _VirtusizeInPageStandardState extends State { _hasError = true; }); }); + + // Start timeout timer for product data check + _startProductDataCheckTimeout(); + } + + void _startProductDataCheckTimeout() { + _productDataCheckTimeout?.cancel(); + _productDataCheckTimedOut = false; + + _productDataCheckTimeout = Timer(Duration(seconds: 10), () { + if (!mounted) return; + if (_productDataCheck == null) { + setState(() { + _productDataCheckTimedOut = true; + }); + } + }); + } + + @override + void didUpdateWidget(VirtusizeInPageStandard oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.product.externalProductId != + widget.product.externalProductId) { + setState(() { + _productDataCheck = null; + _hasError = false; + _isLoading = true; + _showUserProductImage = false; + _storeProduct = null; + _userProduct = null; + _topRecText = null; + _bottomRecText = null; + }); + _startProductDataCheckTimeout(); + } } bool compareProduct({ @@ -170,14 +211,23 @@ class _VirtusizeInPageStandardState extends State { _productSubscription.cancel(); _recSubscription.cancel(); _errorSubscription.cancel(); + _productDataCheckTimeout?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { - return _productDataCheck?.isValidProduct == true - ? _buildVSInPageStandard(context) - : SizedBox.shrink(); + // Hide if product data check timed out + if (_productDataCheckTimedOut && _productDataCheck == null) { + return SizedBox.shrink(); + } + + // Show the widget while loading or if the product is valid + // Hide only if PDC confirms the product is invalid + if (_productDataCheck == null || _productDataCheck!.isValidProduct) { + return _buildVSInPageStandard(context); + } + return SizedBox.shrink(); } Future _openVirtusizeWebview() async { diff --git a/pubspec.yaml b/pubspec.yaml index 4b3e71e..be90c69 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: virtusize_flutter_sdk description: Virtusize SDK for Flutter. This SDK helps clients to integrate Virtusize’s size and fit service into their Flutter applications for Android & iOS. -version: 2.2.7 +version: 2.2.8 repository: https://github.com/virtusize/virtusize_flutter_sdk homepage: https://www.virtusize.com/