membership_offer_controller.dart 6.35 KB
import 'dart:math';

import 'package:doublefeel_flutter/core/constants/membership.dart';
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
import 'package:doublefeel_flutter/data/models/pay/apple_pay_order_response.dart';
import 'package:doublefeel_flutter/data/models/pay/pay_models.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:get/get.dart';

import '../../../routes/app_pages.dart';

class MembershipOfferController extends GetxController {
  MembershipOfferController(this._vipApi, this._payApi);

  final VipApi _vipApi;
  final PayApi _payApi;

  final UserPreferencesStorage _userPreferences =
      Get.find<UserPreferencesStorage>();
  final PlatformHostApi _platformHostApi = PlatformHostApi();

  final isUnlocking = false.obs;
  Future<void> unlock() async {
    if (isUnlocking.value) return;

    final productId = yearlyProduct.value?.id;
    final appleProductId = yearlyProduct.value?.appleId;
    if (productId == null || appleProductId == null || appleProductId.isEmpty) {
      AppToast.show(l10n.purchaseProductInfoUnavailable);
      return;
    }
    isUnlocking.value = true;
    try {
      final orderResult = await _payApi.createOrderByApple(productId);
      if (orderResult is! AppSuccess<ApplePayOrderResponse>) {
        if (isFromOnboard) {
          Get.offAllNamed(AppRoutes.home);
        }
        return;
      }

      final orderUuid = orderResult.data.orderInfo?.orderUuid?.trim();
      if (orderUuid == null || orderUuid.isEmpty) {
        AppToast.show(l10n.purchaseOrderInfoUnavailable);
        if (isFromOnboard) {
          Get.offAllNamed(AppRoutes.home);
        }
        return;
      }

      final paymentResult = await _performApplePayment(
        productId: appleProductId,
        orderUuid: orderUuid,
      );
      await _handleApplePaymentResult(paymentResult);
    } finally {
      isUnlocking.value = false;
    }
  }

  Future<void> _handleApplePaymentResult(
    AppleProductPaymentResult? result,
  ) async {
    if (result?.success == true) {
      await _completeSuccessfulPurchase();
      return;
    }
    if (result?.success != false) return;

    AppToast.show(_applePaymentFailureMessage(result!));
    if (isFromOnboard) {
      Get.offAllNamed(AppRoutes.home);
    }
  }

  String _applePaymentFailureMessage(AppleProductPaymentResult result) {
    final errorCode = result.errorCode;
    if (errorCode != null) {
      return _localizedApplePaymentErrorCode(errorCode);
    }

    final errorMessage = _nonEmpty(result.errorMessage);
    if (errorMessage == null) return l10n.purchaseApplePaymentFailed;
    return _localizedApplePaymentErrorMessage(errorMessage);
  }

  String _localizedApplePaymentErrorCode(int errorCode) {
    return switch (errorCode) {
      -1 => l10n.purchaseApplePaymentInvalidOrder,
      -2 => l10n.purchaseApplePaymentProductNotFound,
      -3 => l10n.purchaseApplePaymentCancelled,
      -4 => l10n.purchaseApplePaymentVerificationFailed,
      -5 => l10n.purchaseApplePaymentFailed,
      _ => l10n.purchaseApplePaymentFailed,
    };
  }

  String _localizedApplePaymentErrorMessage(String errorMessage) {
    if (errorMessage == 'missingUUID') {
      return l10n.purchaseApplePaymentInvalidOrder;
    }
    if (errorMessage == 'productNotFound') {
      return l10n.purchaseApplePaymentProductNotFound;
    }
    if (errorMessage == 'userCancelled') {
      return l10n.purchaseApplePaymentCancelled;
    }
    if (errorMessage == 'failedVerification') {
      return l10n.purchaseApplePaymentVerificationFailed;
    }
    if (errorMessage == 'unknown') {
      return l10n.purchaseApplePaymentFailed;
    }
    return errorMessage;
  }

  Future<void> _completeSuccessfulPurchase() async {
    try {
      await _refreshVipInfo();
    } finally {
      if (isFromOnboard) {
        Get.offAllNamed(AppRoutes.home);
      } else {
        Get.offNamed(Routes.PREMIUM_ACTIVATED);
      }
    }
  }

  Future<void> _refreshVipInfo() async {
    final result = await _vipApi.getVipInfo();
    if (result case AppSuccess(data: final vipInfo)) {
      await _userPreferences.updateVipInfo(
        UserPreferencesVipInfo.fromVipInfo(vipInfo),
      );
    }
  }

  Future<AppleProductPaymentResult?> _performApplePayment({
    required String productId,
    required String orderUuid,
  }) async {
    try {
      return await _platformHostApi.performApplePayment(productId, orderUuid);
    } catch (_) {
      return null;
    }
  }

  @override
  void onInit() {
    super.onInit();
    isFromOnboard = Get.arguments?['from'] == 'onboarding';
    // getProductList();
    _refreshVipInfo();
    getProductList();
  }

  var isFromOnboard = false;

  final yearlyProduct = Rxn<PayProduct>();
  final yearlyProductAppleInfo = Rxn<AppleProductInfo>();

  Future<void> getProductList() async {
    final result = await _payApi.getProductList();
    if (result is! AppSuccess<PayProductListResponse>) return;

    final products = (result.data.productList ?? const <PayProduct>[])
        .where((product) => _nonEmpty(product.appleId) != null)
        .toList();
    yearlyProduct.value = products
        .firstWhereOrNull((p) => p.appleId == Membership.yearlyProductId);

    if (yearlyProduct.value != null) {
      final appleProductId = _nonEmpty(yearlyProduct.value!.appleId);
      if (appleProductId == null) return;

      yearlyProductAppleInfo.value = await _requestAppleProductInfo(
          productId: appleProductId,
          baseUnit: yearlyProduct.value!.content?.baseUnit() ?? 1);
    }
  }

  Future<AppleProductInfo?> _requestAppleProductInfo({
    required String productId,
    required int baseUnit,
  }) async {
    try {
      return await _platformHostApi.requestAppleProductInfo(
        productId,
        baseUnit,
      );
    } catch (_) {
      return null;
    }
  }

  String? _nonEmpty(String? value) {
    final trimmed = value?.trim();
    if (trimmed == null || trimmed.isEmpty) return null;
    return trimmed;
  }
}