exec method

  1. @override
Future<void> exec(
  1. ExecContext context
)
override

Run command.

The contents of katana.yaml and the arguments of the command are passed to context.

コマンドを実行します。

contextkatana.yamlの内容やコマンドの引数が渡されます。

Implementation

@override
Future<void> exec(ExecContext context) async {
  final stripe = context.yaml.getAsMap("stripe");
  final gmail = context.yaml.getAsMap("gmail");
  final sendgrid = context.yaml.getAsMap("sendgrid");
  final secretKey = stripe.get("secret_key", "");
  final enableConnect = stripe.get("enable_connect", false);
  final urlScheme =
      stripe.get("url_scheme", "").replaceAll(RegExp(r"://$"), "");
  final emailProvider = stripe.get("email_provider", "sendgrid");
  final threeDSecureRidirectPages =
      stripe.getAsMap("three_d_secure_redirect_page");
  final firebase = context.yaml.getAsMap("firebase");
  final projectId = firebase.get("project_id", "");
  final function = firebase.getAsMap("functions");
  final region = function.get("region", "");
  if (secretKey.isEmpty) {
    error("[stripe]->[secret_key] is empty.");
    return;
  }
  if (urlScheme.isEmpty) {
    error("[stripe]->[url_scheme] is empty.");
    return;
  }
  if (emailProvider.isEmpty) {
    error("[stripe]->[email_provider] is empty.");
  }
  switch (emailProvider) {
    case "gmail":
      final enableGmail = gmail.get("enable", false);
      final gmailUserId = gmail.get("user_id", "");
      final gmailUserPassword = gmail.get("user_password", "");
      if (!enableGmail) {
        error(
          "If [stripe]->[email_provider] is `gmail`, please include [gmail]->[enable].",
        );
        return;
      }
      if (gmailUserId.isEmpty) {
        error(
          "If [stripe]->[email_provider] is `gmail`, please include [gmail]->[user_id].",
        );
        return;
      }
      if (gmailUserPassword.isEmpty) {
        error(
          "If [stripe]->[email_provider] is `gmail`, please include [gmail]->[user_password].",
        );
        return;
      }
      break;
    default:
      final enalbeSendGrid = sendgrid.get("enable", false);
      final sendGridApiKey = sendgrid.get("api_key", "");
      if (!enalbeSendGrid) {
        error(
          "If [stripe]->[email_provider] is `sendgrid`, please include [sendgrid]->[enable].",
        );
        return;
      }
      if (sendGridApiKey.isEmpty) {
        error(
          "If [stripe]->[email_provider] is `sendgrid`, please include [sendgrid]->[api_key].",
        );
        return;
      }
      break;
  }
  if (projectId.isEmpty) {
    error(
      "The item [firebase]->[project_id] is missing. Please provide the Firebase project ID for the configuration.",
    );
    return;
  }
  if (region.isEmpty) {
    error(
      "The item [firebase]->[functions]->[region] is missing. Please provide the region for the configuration.",
    );
    return;
  }
  final firebaseDir = Directory("firebase");
  if (!firebaseDir.existsSync()) {
    error(
      "The directory `firebase` does not exist. Initialize Firebase by executing Firebase init.",
    );
    return;
  }
  final functionsDir = Directory("firebase/functions");
  if (!functionsDir.existsSync()) {
    error(
      "The directory `firebase/functions` does not exist. Initialize Firebase by executing Firebase init.",
    );
    return;
  }
  final hostingDir = Directory("firebase/hosting");
  if (!hostingDir.existsSync()) {
    error(
      "The directory `firebase/hosting` does not exist. Initialize Firebase by executing Firebase init.",
    );
    return;
  }
  await addFlutterImport(
    [
      "masamune_purchase_stripe",
      "katana_functions_firebase",
    ],
  );
  label("Edit AndroidManifest.xml.");
  final file = File("android/app/src/main/AndroidManifest.xml");
  if (!file.existsSync()) {
    throw Exception(
      "AndroidManifest does not exist in `android/app/src/main/AndroidManifest.xml`. Do `katana create` to complete the initial setup of the project.",
    );
  }
  final androidDocument = XmlDocument.parse(await file.readAsString());
  final activity = androidDocument.findAllElements("activity");
  if (activity.isEmpty) {
    throw Exception(
      "The structure of AndroidManifest.xml is broken. Do `katana create` to complete the initial setup of the project.",
    );
  }
  if (!activity.first.children.any((p0) =>
      p0 is XmlElement &&
      p0.name.toString() == "intent-filter" &&
      (p0.findElements("action").firstOrNull?.attributes.any((item) =>
              item.name.toString() == "android:name" &&
              item.value == "android.intent.action.VIEW") ??
          false) &&
      (p0.findElements("data").firstOrNull?.attributes.any((item) =>
              item.name.toString() == "android:scheme" &&
              item.value == urlScheme) ??
          false))) {
    activity.first.children.add(
      XmlElement(
        XmlName("intent-filter"),
        [],
        [
          XmlElement(
            XmlName("action"),
            [
              XmlAttribute(
                XmlName("android:name"),
                "android.intent.action.VIEW",
              ),
            ],
            [],
          ),
          XmlElement(
            XmlName("category"),
            [
              XmlAttribute(
                XmlName("android:name"),
                "android.intent.category.DEFAULT",
              ),
            ],
            [],
          ),
          XmlElement(
            XmlName("category"),
            [
              XmlAttribute(
                XmlName("android:name"),
                "android.intent.category.BROWSABLE",
              ),
            ],
            [],
          ),
          XmlElement(
            XmlName("data"),
            [
              XmlAttribute(
                XmlName("android:scheme"),
                urlScheme,
              ),
            ],
            [],
          ),
        ],
      ),
    );
  }
  await file.writeAsString(
    androidDocument.toXmlString(pretty: true, indent: "    ", newLine: "\n"),
  );
  label("Edit Info.plist.");
  final plist = File("ios/Runner/Info.plist");
  final iosDocument = XmlDocument.parse(await plist.readAsString());
  final dict = iosDocument.findAllElements("dict").firstOrNull;
  if (dict == null) {
    throw Exception(
      "Could not find `dict` element in `ios/Runner/Info.plist`. File is corrupt.",
    );
  }
  final bundleUrlTypes = dict.children.firstWhereOrNull((p0) {
    return p0 is XmlElement &&
        p0.name.toString() == "key" &&
        p0.innerText == "CFBundleURLTypes";
  });
  if (bundleUrlTypes == null) {
    dict.children.addAll(
      [
        XmlElement(
          XmlName("key"),
          [],
          [
            XmlText("CFBundleURLTypes"),
          ],
        ),
        XmlElement(
          XmlName("array"),
          [],
          [
            XmlElement(
              XmlName("dict"),
              [],
              [
                XmlElement(
                  XmlName("key"),
                  [],
                  [
                    XmlText("CFBundleTypeRole"),
                  ],
                ),
                XmlElement(
                  XmlName("string"),
                  [],
                  [
                    XmlText("Editor"),
                  ],
                ),
                XmlElement(
                  XmlName("key"),
                  [],
                  [
                    XmlText("CFBundleURLSchemes"),
                  ],
                ),
                XmlElement(
                  XmlName("array"),
                  [],
                  [
                    XmlElement(
                      XmlName("string"),
                      [],
                      [
                        XmlText(urlScheme),
                      ],
                    ),
                  ],
                ),
              ],
            ),
          ],
        ),
      ],
    );
  } else {
    final urlSchemeArray = bundleUrlTypes.nextElementSibling;
    if (urlSchemeArray == null) {
      throw Exception(
        "Could not find `CFBundleURLTypes` value element in `ios/Runner/Info.plist`. File is corrupt.",
      );
    }
    if (!urlSchemeArray.children.any(
      (p1) =>
          p1 is XmlElement &&
          p1.children.any((p2) =>
              p2 is XmlElement &&
              p2.name.toString() == "array" &&
              p2.children.any((p3) =>
                  p3 is XmlElement &&
                  p3.name.toString() == "string" &&
                  p3.innerText == urlScheme)),
    )) {
      urlSchemeArray.children.add(
        XmlElement(
          XmlName("dict"),
          [],
          [
            XmlElement(
              XmlName("key"),
              [],
              [
                XmlText("CFBundleTypeRole"),
              ],
            ),
            XmlElement(
              XmlName("string"),
              [],
              [
                XmlText("Editor"),
              ],
            ),
            XmlElement(
              XmlName("key"),
              [],
              [
                XmlText("CFBundleURLSchemes"),
              ],
            ),
            XmlElement(
              XmlName("array"),
              [],
              [
                XmlElement(
                  XmlName("string"),
                  [],
                  [
                    XmlText(urlScheme),
                  ],
                ),
              ],
            ),
          ],
        ),
      );
    }
  }
  await plist.writeAsString(
    iosDocument.toXmlString(pretty: true, indent: "\t", newLine: "\n"),
  );
  label("Configuration Webhooks.");
  String? webHookSecret;
  String? webHookConnectSecret;
  final encodedApiSecret = base64Encode(utf8.encode("$secretKey:"));
  final endpointsRes = await Api.get(
    "https://api.stripe.com/v1/webhook_endpoints",
    headers: {"Authorization": "Basic $encodedApiSecret"},
  );
  if (endpointsRes.statusCode != 200) {
    error("Api secret is invalid.");
    return;
  }
  final endpoints = jsonDecodeAsMap(endpointsRes.body).getAsList("data");
  if (endpoints.isNotEmpty) {
    for (final endpoint in endpoints) {
      final data = endpoint as DynamicMap;
      final id = data.get("id", "");
      if (id.isEmpty) {
        continue;
      }
      await Api.delete(
        "https://api.stripe.com/v1/webhook_endpoints/$id",
        headers: {"Authorization": "Basic $encodedApiSecret"},
      );
    }
  }
  final stripeRes = await Api.post(
    "https://api.stripe.com/v1/webhook_endpoints",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      "Authorization": "Basic $encodedApiSecret"
    },
    body: _formatQueryParamater({
      "url": "https://$region-$projectId.cloudfunctions.net/stripe_webhook",
      "description": "",
      "enabled_events": [
        "customer.subscription.trial_will_end",
        "customer.subscription.deleted",
        "customer.subscription.created",
        "customer.subscription.updated",
        "checkout.session.completed",
        "customer.updated",
        "payment_intent.amount_capturable_updated",
        "payment_intent.payment_failed",
        "payment_intent.requires_action",
        "payment_intent.succeeded",
        "payment_method.updated",
        "payment_method.detached",
      ],
      "connect": "false",
    }),
  );
  final stripeResMap = jsonDecodeAsMap(stripeRes.body);
  webHookSecret = stripeResMap.get("secret", "");
  if (webHookSecret.isEmpty) {
    error(
        "Could not create webhook: https://$region-$projectId.cloudfunctions.net/stripe_webhook");
    return;
  }
  if (enableConnect) {
    final connectRes = await Api.post(
      "https://api.stripe.com/v1/webhook_endpoints",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        "Authorization": "Basic $encodedApiSecret"
      },
      body: _formatQueryParamater({
        "url":
            "https://$region-$projectId.cloudfunctions.net/stripe_webhook_connect",
        "description": "",
        "enabled_events": [
          "account.updated",
        ],
        "connect": "true",
      }),
    );
    final connectResMap = jsonDecodeAsMap(connectRes.body);
    webHookConnectSecret = connectResMap.get("secret", "");
    if (webHookConnectSecret.isEmpty) {
      error(
          "Could not create webhook: https://$region-$projectId.cloudfunctions.net/stripe_webhook_connect");
      return;
    }
  }
  label("Add firebase functions");
  final functions = Fuctions();
  await functions.load();
  if (!functions.functions.any((e) => e.startsWith("stripe"))) {
    functions.functions.add("stripe()");
  }
  if (!functions.functions.any((e) => e.startsWith("stripeWebhook"))) {
    functions.functions.add("stripeWebhook()");
  }
  if (!functions.functions.any((e) => e.startsWith("stripeWebhookSecure"))) {
    functions.functions.add("stripeWebhookSecure()");
  }
  if (enableConnect &&
      !functions.functions.any((e) => e.startsWith("stripeWebhookConnect"))) {
    functions.functions.add("stripeWebhookConnect()");
  }
  switch (emailProvider) {
    case "gmail":
      if (!functions.functions.any((e) => e.startsWith("gmail"))) {
        functions.functions.add("gmail()");
      }
      break;
    default:
      if (!functions.functions.any((e) => e.startsWith("sendGrid"))) {
        functions.functions.add("sendGrid()");
      }
      break;
  }
  await functions.save();
  label("Set firebase functions config.");
  final env = FunctionsEnv();
  await env.load();
  env["PURCHASE_STRIPE_SECRETKEY"] = secretKey;
  env["PURCHASE_STRIPE_EMAILPROVIDER"] = emailProvider;
  env["PURCHASE_STRIPE_USERPATH"] = "plugins/stripe/user";
  env["PURCHASE_STRIPE_PAYMENTPATH"] = "payment";
  env["PURCHASE_STRIPE_PURCHASEPATH"] = "purchase";
  if (webHookSecret.isNotEmpty) {
    env["PURCHASE_STRIPE_WEBHOOKSECRET"] = webHookSecret!;
  }
  if (enableConnect && webHookConnectSecret.isNotEmpty) {
    env["PURCHASE_STRIPE_WEBHOOKCONNECTSECRET"] = webHookConnectSecret!;
  }
  if (emailProvider == "gmail") {
    env["MAIL_GMAIL_ID"] = gmail.get("user_id", "");
    env["MAIL_GMAIL_PASSWORD"] = gmail.get("user_password", "");
  } else {
    env["MAIL_SENDGRID_APIKEY"] = sendgrid.get("api_key", "");
  }
  await env.save();
  context.requestFirebaseDeploy(FirebaseDeployPostActionType.functions);
  // await command(
  //   "Deploy firebase functions.",
  //   [
  //     firebaseCommand,
  //     "deploy",
  //     "--only",
  //     "functions",
  //   ],
  //   workingDirectory: "firebase",
  // );
  for (final locale in threeDSecureRidirectPages.entries) {
    final value = locale.value as Map;
    final successUrl = value.get("success", "");
    final failureUrl = value.get("failure", "");
    if (successUrl.isEmpty) {
      error(
        "The item [stripe]->[three_d_secure_redirect_page]->[${locale.key}]->[success] is missing. Please provide the URL of the Success page.",
      );
      return;
    }
    if (failureUrl.isEmpty) {
      error(
        "The item [stripe]->[three_d_secure_redirect_page]->[${locale.key}]->[failure] is missing. Please provide the URL of the Failure page.",
      );
      return;
    }
    final successResponse = await Api.get(successUrl);
    if (successResponse.statusCode != 200) {
      error(
        "The URL of the Success page is invalid. Please provide the URL of the Success page.",
      );
      return;
    }
    final failureResponse = await Api.get(failureUrl);
    if (failureResponse.statusCode != 200) {
      error(
        "The URL of the Failure page is invalid. Please provide the URL of the Failure page.",
      );
      return;
    }
    final successContent = successResponse.body;
    final failureContent = failureResponse.body;
    final dir = Directory("firebase/hosting/${locale.key}/secure");
    if (!dir.existsSync()) {
      await dir.create(recursive: true);
    }
    final successFile =
        File("firebase/hosting/${locale.key}/secure/success.html");
    await successFile.writeAsString(successContent);
    final failureFile =
        File("firebase/hosting/${locale.key}/secure/failure.html");
    await failureFile.writeAsString(failureContent);
  }
  context.requestFirebaseDeploy(FirebaseDeployPostActionType.hosting);
  // await command(
  //   "Deploy to Firebase Hosting.",
  //   [
  //     firebaseCommand,
  //     "deploy",
  //     "--only",
  //     "hosting",
  //   ],
  //   workingDirectory: "firebase",
  // );
}