Quellcode durchsuchen

[feat]添加苹果登录,修复键盘跳转主App问题

Destiny vor 6 Monaten
Ursprung
Commit
4fb813a305

BIN
assets/images/icon_login_apple.webp


BIN
assets/images/icon_login_dialog_apple.webp


+ 2 - 0
assets/string/base/string.xml

@@ -61,6 +61,7 @@
     <string name="login_resend_code">重新发送</string>
     <string name="login_other_login">其他登录方式</string>
     <string name="wechat">微信</string>
+    <string name="login_apple">Apple登录</string>
     <string name="login_delete_account">注销账号</string>
     <string name="login_delete_account_desc">确认注销后您将失去以下权益 \n1.无法登录本账号
         \n2.您的个人相关数据将被清空,人设设置与个人档案等相关数据将无法恢复。 \n3.会员在有效期内将放弃本账号所有权益
@@ -409,6 +410,7 @@
     <string name="add_hobbies">添加爱好</string>
     <string name="save">保存</string>
     <string name="wechat_login">微信登录</string>
+    <string name="use_apple_login">使用Apple登录</string>
     <string name="phone_login">手机号登录</string>
     <string name="tip">提示</string>
     <string name="privacy_dialog_cancel">不同意</string>

+ 6 - 0
ios/Podfile.lock

@@ -65,6 +65,8 @@ PODS:
   - photo_manager (2.0.0):
     - Flutter
     - FlutterMacOS
+  - sign_in_with_apple (0.0.1):
+    - Flutter
   - SnapKit (5.0.1)
   - sqflite_darwin (0.0.4):
     - Flutter
@@ -114,6 +116,7 @@ DEPENDENCIES:
   - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
   - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
   - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
+  - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
   - SnapKit (~> 5.0.0)
   - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
   - Toast-Swift
@@ -177,6 +180,8 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/permission_handler_apple/ios"
   photo_manager:
     :path: ".symlinks/plugins/photo_manager/ios"
+  sign_in_with_apple:
+    :path: ".symlinks/plugins/sign_in_with_apple/ios"
   sqflite_darwin:
     :path: ".symlinks/plugins/sqflite_darwin/darwin"
   url_launcher_ios:
@@ -221,6 +226,7 @@ SPEC CHECKSUMS:
   path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
   permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
   photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
+  sign_in_with_apple: f3bf75217ea4c2c8b91823f225d70230119b8440
   SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb
   sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
   Toast-Swift: 7a03a532afe3a560d4044bc7c237e2864d295173

+ 62 - 18
ios/Runner/AppDelegate.swift

@@ -3,25 +3,34 @@ import UIKit
 
 @main
 @objc class AppDelegate: FlutterAppDelegate {
+    
+    // 存储冷启动时的URL请求
+    private var hasLaunched: Bool = false
+    
   override func application(
     _ application: UIApplication,
     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
   ) -> Bool {
-      GeneratedPluginRegistrant.register(with: self)
-      
-      let controller = window?.rootViewController as! FlutterViewController
-      FlutterMethodChannelManager.shared.setupMethodChannels(controller: controller)
-      
-      // 创建事件通道
-      let keyboardEventChannel = FlutterEventChannel(name: "keyboard_ios_events",
+        GeneratedPluginRegistrant.register(with: self)
+
+        let controller = window?.rootViewController as! FlutterViewController
+        FlutterMethodChannelManager.shared.setupMethodChannels(controller: controller)
+
+        // 创建事件通道
+        let keyboardEventChannel = FlutterEventChannel(name: "keyboard_ios_events",
                                                         binaryMessenger: controller.binaryMessenger)
-      // 设置事件处理器
-      let keyboardStreamHandler = KeyboardStreamHandler()
-      keyboardEventChannel.setStreamHandler(keyboardStreamHandler)
-      // 开始监听键盘切换
-      startMonitoringKeyboardChanges()
+        // 设置事件处理器
+        let keyboardStreamHandler = KeyboardStreamHandler()
+        keyboardEventChannel.setStreamHandler(keyboardStreamHandler)
+        // 开始监听键盘切换
+        startMonitoringKeyboardChanges()
+      
+        // 延迟处理
+        DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
+            self.hasLaunched = true
+        }
       
-      return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
   }
     
     override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
@@ -29,15 +38,50 @@ import UIKit
         let path = url.path
         if let channel = FlutterMethodChannelManager.shared.keyboardChannel {
             if path == "/login" {
-                channel.invokeMethod("navigateToLogin", arguments: nil)
+                if hasLaunched {
+                    channel.invokeMethod("navigateToLogin", arguments: nil)
+                } else {
+                    // 延迟处理
+                    DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
+                        channel.invokeMethod("navigateToLogin", arguments: nil)
+                    }
+                }
             } else if path == "/member" {
-                channel.invokeMethod("navigateToMember", arguments: nil)
+                if hasLaunched {
+                    channel.invokeMethod("navigateToMember", arguments: nil)
+                } else {
+                    // 延迟处理
+                    DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
+                        channel.invokeMethod("navigateToMember", arguments: nil)
+                    }
+                }
             } else if path == "/character/market" {
-                channel.invokeMethod("navigateToCharacterMarket", arguments: nil)
+                if hasLaunched {
+                    channel.invokeMethod("navigateToCharacterMarket", arguments: nil)
+                } else {
+                    // 延迟处理
+                    DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
+                        channel.invokeMethod("navigateToCharacterMarket", arguments: nil)
+                    }
+                }
             } else if path == "/intimacy" {
-                channel.invokeMethod("navigateToIntimacy", arguments: nil)
+                if hasLaunched {
+                    channel.invokeMethod("navigateToIntimacy", arguments: nil)
+                } else {
+                    // 延迟处理
+                    DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) {
+                        channel.invokeMethod("navigateToIntimacy", arguments: nil)
+                    }
+                }
             } else if path == "/character/custom" {
-                channel.invokeMethod("navigateToCustomCharacter", arguments: nil)
+                if hasLaunched {
+                    channel.invokeMethod("navigateToCustomCharacter", arguments: nil)
+                } else {
+                    // 延迟处理
+                    DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
+                        channel.invokeMethod("navigateToCustomCharacter", arguments: nil)
+                    }
+                }
             }
         }
 

+ 4 - 0
ios/Runner/Runner.entitlements

@@ -2,6 +2,10 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>com.apple.developer.applesignin</key>
+	<array>
+		<string>Default</string>
+	</array>
 	<key>com.apple.developer.associated-domains</key>
 	<array>
 		<string>applinks:youyue.shop</string>

+ 7 - 0
lib/data/api/atmob_api.dart

@@ -1,5 +1,6 @@
 import 'package:dio/dio.dart';
 import 'package:keyboard/base/base_response.dart';
+import 'package:keyboard/data/api/request/apple_login_request.dart';
 import 'package:keyboard/data/api/request/character_add_request.dart';
 import 'package:keyboard/data/api/request/character_custom_add_request.dart';
 import 'package:keyboard/data/api/request/character_custom_delete_request.dart';
@@ -100,6 +101,12 @@ abstract class AtmobApi {
     @Body() WechatLoginRequest request,
   );
 
+  // 苹果登录
+  @POST("/central/open/v1/user/apple/login")
+  Future<BaseResponse<LoginResponse>> loginUserAppleLogin(
+      @Body() AppleLoginRequest request,
+  );
+
   // 注销账号
   @POST("/central/open/v1/user/deprecate")
   Future<BaseResponse> deprecate(@Body() AppBaseRequest request);

+ 33 - 0
lib/data/api/atmob_api.g.dart

@@ -115,6 +115,39 @@ class _AtmobApi implements AtmobApi {
   }
 
   @override
+  Future<BaseResponse<LoginResponse>> loginUserAppleLogin(
+    AppleLoginRequest request,
+  ) async {
+    final _extra = <String, dynamic>{};
+    final queryParameters = <String, dynamic>{};
+    final _headers = <String, dynamic>{};
+    final _data = <String, dynamic>{};
+    _data.addAll(request.toJson());
+    final _options = _setStreamType<BaseResponse<LoginResponse>>(
+      Options(method: 'POST', headers: _headers, extra: _extra)
+          .compose(
+            _dio.options,
+            '/central/open/v1/user/apple/login',
+            queryParameters: queryParameters,
+            data: _data,
+          )
+          .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
+    );
+    final _result = await _dio.fetch<Map<String, dynamic>>(_options);
+    late BaseResponse<LoginResponse> _value;
+    try {
+      _value = BaseResponse<LoginResponse>.fromJson(
+        _result.data!,
+        (json) => LoginResponse.fromJson(json as Map<String, dynamic>),
+      );
+    } on Object catch (e, s) {
+      errorLogger?.logError(e, s, _options);
+      rethrow;
+    }
+    return _value;
+  }
+
+  @override
   Future<BaseResponse<dynamic>> deprecate(AppBaseRequest request) async {
     final _extra = <String, dynamic>{};
     final queryParameters = <String, dynamic>{};

+ 23 - 0
lib/data/api/request/apple_login_request.dart

@@ -0,0 +1,23 @@
+import 'package:json_annotation/json_annotation.dart';
+
+import '../../../base/app_base_request.dart';
+
+
+part 'apple_login_request.g.dart';
+
+@JsonSerializable()
+class AppleLoginRequest extends AppBaseRequest {
+  @JsonKey(name: "userIdentity")
+  String userIdentity;
+
+  @JsonKey(name: "authorizationCode")
+  String authorizationCode;
+
+  @JsonKey(name: "identityToken")
+  String identityToken;
+
+  AppleLoginRequest(this.userIdentity, this.authorizationCode, this.identityToken);
+
+  @override
+  Map<String, dynamic> toJson() => _$AppleLoginRequestToJson(this);
+}

+ 73 - 0
lib/data/api/request/apple_login_request.g.dart

@@ -0,0 +1,73 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'apple_login_request.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+AppleLoginRequest _$AppleLoginRequestFromJson(Map<String, dynamic> json) =>
+    AppleLoginRequest(
+        json['userIdentity'] as String,
+        json['authorizationCode'] as String,
+        json['identityToken'] as String,
+      )
+      ..appPlatform = (json['appPlatform'] as num).toInt()
+      ..os = json['os'] as String
+      ..osVersion = json['osVersion'] as String
+      ..packageName = json['packageName'] as String?
+      ..appVersionName = json['appVersionName'] as String?
+      ..appVersionCode = (json['appVersionCode'] as num?)?.toInt()
+      ..channelName = json['channelName'] as String?
+      ..appId = (json['appId'] as num?)?.toInt()
+      ..tgPlatform = (json['tgPlatform'] as num?)?.toInt()
+      ..oaid = json['oaid'] as String?
+      ..aaid = json['aaid'] as String?
+      ..androidId = json['androidId'] as String?
+      ..imei = json['imei'] as String?
+      ..simImei0 = json['simImei0'] as String?
+      ..simImei1 = json['simImei1'] as String?
+      ..mac = json['mac'] as String?
+      ..idfa = json['idfa'] as String?
+      ..idfv = json['idfv'] as String?
+      ..machineId = json['machineId'] as String?
+      ..brand = json['brand'] as String?
+      ..model = json['model'] as String?
+      ..wifiName = json['wifiName'] as String?
+      ..region = json['region'] as String?
+      ..locLng = (json['locLng'] as num?)?.toDouble()
+      ..locLat = (json['locLat'] as num?)?.toDouble()
+      ..authToken = json['authToken'] as String?;
+
+Map<String, dynamic> _$AppleLoginRequestToJson(AppleLoginRequest instance) =>
+    <String, dynamic>{
+      'appPlatform': instance.appPlatform,
+      'os': instance.os,
+      'osVersion': instance.osVersion,
+      'packageName': instance.packageName,
+      'appVersionName': instance.appVersionName,
+      'appVersionCode': instance.appVersionCode,
+      'channelName': instance.channelName,
+      'appId': instance.appId,
+      'tgPlatform': instance.tgPlatform,
+      'oaid': instance.oaid,
+      'aaid': instance.aaid,
+      'androidId': instance.androidId,
+      'imei': instance.imei,
+      'simImei0': instance.simImei0,
+      'simImei1': instance.simImei1,
+      'mac': instance.mac,
+      'idfa': instance.idfa,
+      'idfv': instance.idfv,
+      'machineId': instance.machineId,
+      'brand': instance.brand,
+      'model': instance.model,
+      'wifiName': instance.wifiName,
+      'region': instance.region,
+      'locLng': instance.locLng,
+      'locLat': instance.locLat,
+      'authToken': instance.authToken,
+      'userIdentity': instance.userIdentity,
+      'authorizationCode': instance.authorizationCode,
+      'identityToken': instance.identityToken,
+    };

+ 1 - 1
lib/data/consts/constants.dart

@@ -13,7 +13,7 @@ class Constants {
 
   static const String envProd = 'prod';
 
-  static const String _devBaseUrl = "http://192.168.10.113:8880";
+  static const String _devBaseUrl = "https://ws00.by.takin.cc";
 
   static const String _testBaseUrl = "http://42.193.245.11";
 

+ 36 - 0
lib/data/repository/account_repository.dart

@@ -3,11 +3,13 @@ import 'dart:async';
 import 'package:get/get.dart';
 import 'package:injectable/injectable.dart';
 import 'package:keyboard/data/api/atmob_api.dart';
+import 'package:keyboard/data/api/request/apple_login_request.dart';
 import 'package:keyboard/data/api/request/user_info_setting_request.dart';
 import 'package:keyboard/data/api/request/wechat_login_request.dart';
 import 'package:keyboard/data/api/response/wechat_login_response.dart';
 import 'package:keyboard/data/bean/member_info.dart';
 import 'package:keyboard/data/repository/keyboard_repository.dart';
+import 'package:keyboard/utils/method_chanel_ios_util.dart';
 
 import '../../base/app_base_request.dart';
 import '../../di/get_it.dart';
@@ -127,6 +129,27 @@ class AccountRepository {
     });
   }
 
+  Future<LoginResponse> appleLogin(String userIdentity, String authorizationCode, String identityToken) {
+    if (_errorCodeTimes >= 5) {
+      throw LoginTooOftenException();
+    }
+    return atmobApi
+        .loginUserAppleLogin(AppleLoginRequest(userIdentity, authorizationCode, identityToken))
+        .then(HttpHandler.handle(true))
+        .then((response) {
+      _errorCodeTimes = 0;
+      onAppleLoginSuccess(response.authToken);
+      return response;
+    })
+        .catchError((error) {
+      if (error is ServerErrorException &&
+          error.code == ErrorCode.verificationCodeError) {
+        _errorCodeTimes++;
+      }
+      throw error;
+    });
+  }
+
   Future<void> deprecateAccount() {
     return atmobApi.deprecate(AppBaseRequest()).then(HttpHandler.handle(true));
   }
@@ -208,6 +231,7 @@ class AccountRepository {
 
   void onLoginSuccess(String phoneNum, String authToken) {
     GravityHelper.onLogin();
+    MethodChanelIOSUtil.saveAuthToken(authToken);
     AccountRepository.token = authToken;
     loginPhoneNum.value = phoneNum;
     refreshUserInfo();
@@ -222,17 +246,29 @@ class AccountRepository {
 
   void onWechatLoginSuccess(String authToken) {
     AccountRepository.token = authToken;
+    MethodChanelIOSUtil.saveAuthToken(authToken);
     GravityHelper.onLogin();
     refreshUserInfo();
     KVUtil.putString(keyAccountLoginToken, authToken);
     keyboardRepository.refreshData();
     // 微信登录,通知键盘刷新数据
     _notifyKeyboardPluginRefreshData();
+  }
 
+  void onAppleLoginSuccess(String authToken) {
+    AccountRepository.token = authToken;
+    MethodChanelIOSUtil.saveAuthToken(authToken);
+    GravityHelper.onLogin();
+    refreshUserInfo();
+    KVUtil.putString(keyAccountLoginToken, authToken);
+    keyboardRepository.refreshData();
+    // 苹果登录,通知键盘刷新数据
+    _notifyKeyboardPluginRefreshData();
   }
 
   void logout() {
     token = null;
+    MethodChanelIOSUtil.clearAuthToken();
     KVUtil.putString(keyAccountLoginPhoneNum, null);
     KVUtil.putString(keyAccountLoginToken, null);
     memberStatusInfo.value = null;

+ 62 - 0
lib/dialog/login/login_dialog_controller.dart

@@ -7,9 +7,11 @@ import 'package:get/get.dart';
 import 'package:injectable/injectable.dart';
 import 'package:keyboard/base/base_controller.dart';
 import 'package:keyboard/data/repository/account_repository.dart';
+import 'package:keyboard/dialog/loading_dialog.dart';
 import 'package:keyboard/module/login/login_page.dart';
 import 'package:keyboard/utils/atmob_log.dart';
 import 'package:keyboard/utils/toast_util.dart';
+import 'package:sign_in_with_apple/sign_in_with_apple.dart';
 
 import '../../data/consts/error_code.dart';
 import '../../data/consts/event_report.dart';
@@ -85,6 +87,66 @@ class LoginDialogController extends BaseController {
     );
   }
 
+  // 苹果登录点击
+  void clickAppleLogin() async {
+    if (!_isAgree.value) {
+      PrivacyAgreementDialog.show(
+        btnConfirm: () async {
+          _isAgree.value = true;
+          clickAppleLogin();
+        },
+      );
+      ToastUtil.show(StringName.loginAgreePrivacy);
+      return;
+    }
+
+    CustomLoadingDialog.show();
+
+    AuthorizationCredentialAppleID credential;
+    try {
+      credential = await SignInWithApple.getAppleIDCredential(
+        scopes: [
+          AppleIDAuthorizationScopes.email,
+          AppleIDAuthorizationScopes.fullName,
+        ],
+      );
+    } catch (error) {
+      CustomLoadingDialog.hide();
+      ToastUtil.show(StringName.loginFailedToast);
+      return;
+    }
+
+    var userIdentity = credential.userIdentifier ?? "";
+    var authorizationCode = credential.authorizationCode ?? "";
+    var identityToken = credential.identityToken ?? "";
+
+    accountRepository
+        .appleLogin(userIdentity, authorizationCode, identityToken)
+        .then((data) {
+      CustomLoadingDialog.hide();
+      EventHandler.report(EventId.event_04003);
+      SmartDialog.dismiss(tag: LoginDialog.TAG);
+      ToastUtil.show(StringName.loginSuccess);
+    })
+        .catchError((error) {
+      CustomLoadingDialog.hide();
+      EventHandler.report(EventId.event_04004);
+      if (error is LoginTooOftenException) {
+        ToastUtil.show(StringName.loginTooOftenToast);
+        return;
+      }
+      if (error is ServerErrorException) {
+        if (error.code == ErrorCode.verificationCodeError) {
+          ToastUtil.show(StringName.loginVerificationCodeErrorToast);
+        } else {
+          ToastUtil.show(error.message);
+        }
+      } else {
+        ErrorHandler.toastError(error);
+      }
+    });
+  }
+
   void clickClose() {
     AtmobLog.d(tag, "clickClose");
     SmartDialog.dismiss(tag: LoginDialog.TAG);

+ 42 - 1
lib/dialog/login/login_dialog_view.dart

@@ -48,6 +48,8 @@ class LoginDialogView extends BaseView<LoginDialogController> {
                     children: [
                       SizedBox(height: 131.h),
                       _buildWeChatButton(),
+                      SizedBox(height: 12.h),
+                      _buildAppleButton(),
                       SizedBox(height: 31.h),
                       _buildPhoneLoginButton(),
                       SizedBox(height: 10.h),
@@ -117,7 +119,7 @@ class LoginDialogView extends BaseView<LoginDialogController> {
         width: 312.w,
         height: 48.h,
         decoration: ShapeDecoration(
-          color: const Color(0xFF31DB78),
+          color: const Color(0xFF32DB78),
           shape: RoundedRectangleBorder(
             borderRadius: BorderRadius.circular(50.r),
           ),
@@ -147,6 +149,45 @@ class LoginDialogView extends BaseView<LoginDialogController> {
     );
   }
 
+  Widget _buildAppleButton() {
+    return InkWell(
+      onTap: () {
+        controller.clickAppleLogin();
+      },
+      child: Container(
+        width: 312.w,
+        height: 48.h,
+        decoration: ShapeDecoration(
+          color: const Color(0xFFF0F1F3),
+          shape: RoundedRectangleBorder(
+            borderRadius: BorderRadius.circular(50.r),
+          ),
+        ),
+        child: Row(
+          mainAxisSize: MainAxisSize.min,
+          mainAxisAlignment: MainAxisAlignment.center,
+          crossAxisAlignment: CrossAxisAlignment.center,
+          spacing: 4,
+          children: [
+            Assets.images.iconLoginDialogApple.image(
+              width: 14.w,
+              height: 14.w,
+            ),
+            Text(
+              StringName.useAppleLogin,
+              textAlign: TextAlign.center,
+              style: TextStyle(
+                color: Color(0xFF000000).withAlpha(204),
+                fontSize: 16.sp,
+                fontWeight: FontWeight.w500,
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
   Widget _buildPrivacy() {
     return Row(
       mainAxisAlignment: MainAxisAlignment.center,

+ 1 - 0
lib/main.dart

@@ -48,6 +48,7 @@ void main() async {
   } else if (PlatformUtil.isIOS) {
     MethodChanelIOSUtil.initialize();
   }
+
   AssetLottie(Assets.anim.animSurpriseDialogData).load();
   runApp(const MyApp());
   //檢查地址

+ 59 - 2
lib/module/login/login_controller.dart

@@ -2,7 +2,9 @@ import 'package:flutter/cupertino.dart';
 import 'package:get/get.dart';
 import 'package:injectable/injectable.dart';
 import 'package:keyboard/base/base_controller.dart';
+import 'package:keyboard/dialog/loading_dialog.dart';
 import 'package:keyboard/dialog/login/login_dialog.dart';
+import 'package:sign_in_with_apple/sign_in_with_apple.dart';
 
 import '../../data/consts/error_code.dart';
 import '../../data/consts/event_report.dart';
@@ -121,7 +123,6 @@ class LoginController extends BaseController {
           EventHandler.report(EventId.event_04003);
           Get.back();
           ToastUtil.show(StringName.loginSuccess);
-
         })
         .catchError((error) {
           EventHandler.report(EventId.event_04004);
@@ -173,8 +174,64 @@ class LoginController extends BaseController {
       LoginDialog.show();
       return;
     }
-
   }
 
+  // 苹果登录点击
+  void clickAppleLogin() async {
+    if (!_isAgree.value) {
+      PrivacyAgreementDialog.show(
+        btnConfirm: () async {
+          _isAgree.value = true;
+          clickAppleLogin();
+        },
+      );
+      ToastUtil.show(StringName.loginAgreePrivacy);
+      return;
+    }
 
+    CustomLoadingDialog.show();
+    AuthorizationCredentialAppleID credential;
+    try {
+      credential = await SignInWithApple.getAppleIDCredential(
+        scopes: [
+          AppleIDAuthorizationScopes.email,
+          AppleIDAuthorizationScopes.fullName,
+        ],
+      );
+    } catch (error) {
+      CustomLoadingDialog.hide();
+      ToastUtil.show(StringName.loginFailedToast);
+      return;
+    }
+
+    var userIdentity = credential.userIdentifier ?? "";
+    var authorizationCode = credential.authorizationCode ?? "";
+    var identityToken = credential.identityToken ?? "";
+
+    accountRepository
+        .appleLogin(userIdentity, authorizationCode, identityToken)
+        .then((data) {
+          CustomLoadingDialog.hide();
+          EventHandler.report(EventId.event_04003);
+          Get.back();
+          ToastUtil.show(StringName.loginSuccess);
+        })
+        .catchError((error) {
+          CustomLoadingDialog.hide();
+          EventHandler.report(EventId.event_04004);
+          if (error is LoginTooOftenException) {
+            ToastUtil.show(StringName.loginTooOftenToast);
+            return;
+          }
+          if (error is ServerErrorException) {
+            if (error.code == ErrorCode.verificationCodeError) {
+              ToastUtil.show(StringName.loginVerificationCodeErrorToast);
+            } else {
+              ToastUtil.show(error.message);
+            }
+          } else {
+            ErrorHandler.toastError(error);
+          }
+        });
+  }
 }

+ 160 - 112
lib/module/login/login_page.dart

@@ -37,38 +37,39 @@ class LoginPage extends BasePage<LoginController> {
   @override
   Widget buildBody(BuildContext context) {
     return PopScope(
-        canPop: false,
-        onPopInvokedWithResult: (didPop, result) async {
-          if (didPop) {
-            return;
-          }
-          controller.clickBack();
-        },
-        child: Stack(
-      children: [
-        Assets.images.bgLogin.image(fit: BoxFit.contain, width: 360.w),
-        _buildTitle(),
-        SafeArea(
-          child: Column(
-            children: [
-              SizedBox(height: 225.w),
-              buildPhoneTextFiled(),
-              SizedBox(height: 16.w),
-              buildCodeTextFiled(),
-              SizedBox(height: 46.w),
-              buildLoginButton(),
-              SizedBox(height: 25.w),
-              _buildPrivacy(),
-              Spacer(),
-              PlatformUtil.isAndroid ? buildOtherLogin() : Container(),
-              SizedBox(height: 20.w),
-            ],
+      canPop: false,
+      onPopInvokedWithResult: (didPop, result) async {
+        if (didPop) {
+          return;
+        }
+        controller.clickBack();
+      },
+      child: Stack(
+        children: [
+          Assets.images.bgLogin.image(fit: BoxFit.contain, width: 360.w),
+          _buildTitle(),
+          SafeArea(
+            child: Column(
+              children: [
+                SizedBox(height: 225.w),
+                buildPhoneTextFiled(),
+                SizedBox(height: 16.w),
+                buildCodeTextFiled(),
+                SizedBox(height: 46.w),
+                buildLoginButton(),
+                SizedBox(height: 25.w),
+                _buildPrivacy(),
+                Spacer(),
+                buildOtherLogin(),
+                SizedBox(height: 20.w),
+              ],
+            ),
           ),
-        ),
-
-      ],
-    ));
+        ],
+      ),
+    );
   }
+
   _buildTitle() {
     return SafeArea(
       child: Container(
@@ -89,6 +90,7 @@ class LoginPage extends BasePage<LoginController> {
       ),
     );
   }
+
   Widget buildPhoneTextFiled() {
     return Container(
       height: 48.w,
@@ -105,7 +107,7 @@ class LoginPage extends BasePage<LoginController> {
               cursorHeight: 20.w,
               style: TextStyle(
                 fontSize: 14.sp,
-                color: Colors.black.withAlpha( 204),
+                color: Colors.black.withAlpha(204),
                 fontWeight: FontWeight.w500,
               ),
               maxLines: 1,
@@ -154,7 +156,7 @@ class LoginPage extends BasePage<LoginController> {
               cursorHeight: 20.w,
               style: TextStyle(
                 fontSize: 14.sp,
-                color: Colors.black.withAlpha( 204),
+                color: Colors.black.withAlpha(204),
                 fontWeight: FontWeight.w500,
               ),
               maxLines: 1,
@@ -205,15 +207,15 @@ class LoginPage extends BasePage<LoginController> {
             } else {
               txt = StringName.loginResendCode;
             }
-
           }
           return Text(
             txt,
             style: TextStyle(
               fontSize: 14.sp,
-              color: controller.isFirstSend
-                  ? Colors.black.withAlpha(204)
-                  : Color(0xCC7D46FC)
+              color:
+                  controller.isFirstSend
+                      ? Colors.black.withAlpha(204)
+                      : Color(0xCC7D46FC),
             ),
           );
         }),
@@ -267,65 +269,68 @@ class LoginPage extends BasePage<LoginController> {
               controller.clickAgree();
             },
             child: Padding(
-              padding: EdgeInsets.symmetric(vertical: 20.w,horizontal: 20.w),
+              padding: EdgeInsets.symmetric(vertical: 20.w, horizontal: 20.w),
               child:
-              controller.isAgree
-                  ? Assets.images.iconLoginAgreePrivacy.image(
-                width: 14.w,
-                height: 14.w,
-              )
-                  : Container(
-                width: 14.w,
-                height: 14.w,
-                child: Container(
-                  decoration: BoxDecoration(
-                    shape: BoxShape.circle,
-                    border: Border.all(
-                      color: Colors.black.withAlpha(153),
-                      width: 1.w,
-                    ),
-                  ),
-                ),
-              ),
+                  controller.isAgree
+                      ? Assets.images.iconLoginAgreePrivacy.image(
+                        width: 14.w,
+                        height: 14.w,
+                      )
+                      : Container(
+                        width: 14.w,
+                        height: 14.w,
+                        child: Container(
+                          decoration: BoxDecoration(
+                            shape: BoxShape.circle,
+                            border: Border.all(
+                              color: Colors.black.withAlpha(153),
+                              width: 1.w,
+                            ),
+                          ),
+                        ),
+                      ),
             ),
           );
         }),
-        Transform.translate(offset: Offset(-17.w,0),child:  Text.rich(
-          TextSpan(
-            children: [
-              TextSpan(
-                text: StringName.textSpanIHaveReadAndAgree,
-                style: TextStyle(
-                  color: Colors.black.withAlpha(128),
+        Transform.translate(
+          offset: Offset(-17.w, 0),
+          child: Text.rich(
+            TextSpan(
+              children: [
+                TextSpan(
+                  text: StringName.textSpanIHaveReadAndAgree,
+                  style: TextStyle(
+                    color: Colors.black.withAlpha(128),
+                    fontSize: 12.sp,
+                    fontWeight: FontWeight.w400,
+                  ),
+                ),
+                ClickTextSpan(
+                  text: StringName.textSpanPrivacyPolicy,
+                  url: WebUrl.privacyPolicy,
                   fontSize: 12.sp,
-                  fontWeight: FontWeight.w400,
+                  color: Colors.black.withAlpha(204),
                 ),
-              ),
-              ClickTextSpan(
-                text: StringName.textSpanPrivacyPolicy,
-                url: WebUrl.privacyPolicy,
-                fontSize: 12.sp,
-                color: Colors.black.withAlpha(204),
-              ),
 
-              TextSpan(
-                text: StringName.textSpanAnd,
-                style: TextStyle(
-                  color: Colors.black.withAlpha(128),
-                  fontSize: 12.sp,
-                  fontWeight: FontWeight.w400,
+                TextSpan(
+                  text: StringName.textSpanAnd,
+                  style: TextStyle(
+                    color: Colors.black.withAlpha(128),
+                    fontSize: 12.sp,
+                    fontWeight: FontWeight.w400,
+                  ),
                 ),
-              ),
 
-              ClickTextSpan(
-                text: StringName.textSpanServiceTerms,
-                url: WebUrl.serviceAgreement,
-                color: Colors.black.withAlpha(204),
-                fontSize: 12.sp,
-              ),
-            ],
+                ClickTextSpan(
+                  text: StringName.textSpanServiceTerms,
+                  url: WebUrl.serviceAgreement,
+                  color: Colors.black.withAlpha(204),
+                  fontSize: 12.sp,
+                ),
+              ],
+            ),
           ),
-        ),),
+        ),
       ],
     );
   }
@@ -343,35 +348,78 @@ class LoginPage extends BasePage<LoginController> {
             ),
           ),
           SizedBox(height: 18.h),
-          Material(
-            color: Colors.white,
-            shape: const CircleBorder(),
-            child: InkWell(
-              customBorder: const CircleBorder(), // 保证点击区域是圆的
-              onTap: () {
-                controller.clickWxLogin();
-              },
-              child: SizedBox(
-                width: 44.w,
-                height: 44.w,
-                child: Center(
-                  child: Assets.images.iconWechatLogoBlack.image(
-                    width: 22.w,
-                    height: 22.w,
+          Row(
+            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+            children: [
+              Column(
+                children: [
+                  Material(
+                    color: Colors.white,
+                    shape: const CircleBorder(),
+                    child: InkWell(
+                      customBorder: const CircleBorder(), // 保证点击区域是圆的
+                      onTap: () {
+                        controller.clickAppleLogin();
+                      },
+                      child: SizedBox(
+                        width: 44.w,
+                        height: 44.w,
+                        child: Center(
+                          child: Assets.images.iconLoginDialogApple.image(
+                            width: 22.w,
+                            height: 22.w,
+                          ),
+                        ),
+                      ),
+                    ),
                   ),
-                ),
+                  SizedBox(height: 6.w),
+                  Text(
+                    StringName.loginApple,
+                    textAlign: TextAlign.center,
+                    style: TextStyle(
+                      color: Colors.black.withValues(alpha: 0.5),
+                      fontSize: 12.sp,
+                      height: 0,
+                    ),
+                  ),
+                ],
               ),
-            ),
-          ),
-          SizedBox(height: 6.w),
-          Text(
-            StringName.wechat,
-            textAlign: TextAlign.center,
-            style: TextStyle(
-              color: Colors.black.withValues(alpha: 0.5),
-              fontSize: 12.sp,
-              height: 0,
-            ),
+              Column(
+                children: [
+                  Material(
+                    color: Colors.white,
+                    shape: const CircleBorder(),
+                    child: InkWell(
+                      customBorder: const CircleBorder(), // 保证点击区域是圆的
+                      onTap: () {
+                        controller.clickWxLogin();
+                      },
+                      child: SizedBox(
+                        width: 44.w,
+                        height: 44.w,
+                        child: Center(
+                          child: Assets.images.iconWechatLogoBlack.image(
+                            width: 22.w,
+                            height: 22.w,
+                          ),
+                        ),
+                      ),
+                    ),
+                  ),
+                  SizedBox(height: 6.w),
+                  Text(
+                    StringName.wechat,
+                    textAlign: TextAlign.center,
+                    style: TextStyle(
+                      color: Colors.black.withValues(alpha: 0.5),
+                      fontSize: 12.sp,
+                      height: 0,
+                    ),
+                  ),
+                ],
+              ),
+            ],
           ),
         ],
       ),

+ 8 - 8
lib/module/mine/mine_controller.dart

@@ -69,11 +69,11 @@ class MineController extends BaseController {
     if (isLogin) {
       UserInfoPage.start();
     } else {
-      if (PlatformUtil.isIOS) {
-        LoginPage.start();
-      } else {
+      // if (PlatformUtil.isIOS) {
+      //   LoginPage.start();
+      // } else {
         LoginDialog.show();
-      }
+      // }
     }
     // KeyboardAndroidPlatform.enableFloatingWindow(true);
     // KeyboardAndroidPlatform.openInputMethodSettings();
@@ -123,11 +123,11 @@ class MineController extends BaseController {
       EventHandler.report(EventId.event_14001);
       UserInfoPage.start();
     } else {
-      if (PlatformUtil.isIOS) {
-        LoginPage.start();
-      } else {
+      // if (PlatformUtil.isIOS) {
+      //   LoginPage.start();
+      // } else {
         LoginDialog.show();
-      }
+      // }
       // LoginPage.start();
       // LoginDialog.show();
     }

+ 10 - 0
lib/resource/assets.gen.dart

@@ -879,6 +879,14 @@ class $AssetsImagesGen {
   AssetGenImage get iconLoginAgreePrivacy =>
       const AssetGenImage('assets/images/icon_login_agree_privacy.webp');
 
+  /// File path: assets/images/icon_login_apple.webp
+  AssetGenImage get iconLoginApple =>
+      const AssetGenImage('assets/images/icon_login_apple.webp');
+
+  /// File path: assets/images/icon_login_dialog_apple.webp
+  AssetGenImage get iconLoginDialogApple =>
+      const AssetGenImage('assets/images/icon_login_dialog_apple.webp');
+
   /// File path: assets/images/icon_login_dialog_close.webp
   AssetGenImage get iconLoginDialogClose =>
       const AssetGenImage('assets/images/icon_login_dialog_close.webp');
@@ -1525,6 +1533,8 @@ class $AssetsImagesGen {
     iconLibra,
     iconLock,
     iconLoginAgreePrivacy,
+    iconLoginApple,
+    iconLoginDialogApple,
     iconLoginDialogClose,
     iconLoginDialogPhoneLogo,
     iconLoginDialogWechatLogoWhite,

+ 4 - 0
lib/resource/string.gen.dart

@@ -50,6 +50,7 @@ class StringName {
   static final String loginResendCode = 'login_resend_code'.tr; // 重新发送
   static final String loginOtherlogin = 'login_other_login'.tr; // 其他登录方式
   static final String wechat = 'wechat'.tr; // 微信
+  static final String loginApple = 'login_apple'.tr; // Apple登录
   static final String loginDeleteAccount = 'login_delete_account'.tr; // 注销账号
   static final String loginDeleteAccountDesc = 'login_delete_account_desc'.tr; // 确认注销后您将失去以下权益 \n1.无法登录本账号 \n2.您的个人相关数据将被清空,人设设置与个人档案等相关数据将无法恢复。 \n3.会员在有效期内将放弃本账号所有权益
   static final String loginDeleteAccountConfirm = 'login_delete_account_confirm'.tr; // 确认注销
@@ -297,6 +298,7 @@ class StringName {
   static final String addHobbies = 'add_hobbies'.tr; // 添加爱好
   static final String save = 'save'.tr; // 保存
   static final String wechatLogin = 'wechat_login'.tr; // 微信登录
+  static final String useAppleLogin = 'use_apple_login'.tr; // 使用Apple登录
   static final String phoneLogin = 'phone_login'.tr; // 手机号登录
   static final String tip = 'tip'.tr; // 提示
   static final String privacyDialogCancel = 'privacy_dialog_cancel'.tr; // 不同意
@@ -424,6 +426,7 @@ class StringMultiSource {
       'login_resend_code': '重新发送',
       'login_other_login': '其他登录方式',
       'wechat': '微信',
+      'login_apple': 'Apple登录',
       'login_delete_account': '注销账号',
       'login_delete_account_desc': '确认注销后您将失去以下权益 \n1.无法登录本账号 \n2.您的个人相关数据将被清空,人设设置与个人档案等相关数据将无法恢复。 \n3.会员在有效期内将放弃本账号所有权益',
       'login_delete_account_confirm': '确认注销',
@@ -671,6 +674,7 @@ class StringMultiSource {
       'add_hobbies': '添加爱好',
       'save': '保存',
       'wechat_login': '微信登录',
+      'use_apple_login': '使用Apple登录',
       'phone_login': '手机号登录',
       'tip': '提示',
       'privacy_dialog_cancel': '不同意',

+ 2 - 0
lib/utils/method_chanel_ios_util.dart

@@ -7,6 +7,7 @@ import 'package:keyboard/module/login/login_page.dart';
 import 'package:keyboard/module/main/main_page.dart';
 import 'package:keyboard/module/store/store_page.dart';
 import 'package:keyboard/utils/default_keyboard_helper.dart';
+import 'package:keyboard/utils/toast_util.dart';
 
 import '../module/intimacy_scale/intimacy_scale_page.dart';
 import '../widget/platform_util.dart';
@@ -27,6 +28,7 @@ class MethodChanelIOSUtil {
   static Future<dynamic> _handleMethod(MethodCall call) async {
     switch (call.method) {
       case 'navigateToLogin':
+        ToastUtil.show("navigateToLogin");
         LoginPage.start();
         break;
       case 'navigateToMember':

+ 24 - 0
pubspec.lock

@@ -1410,6 +1410,30 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "3.0.0"
+  sign_in_with_apple:
+    dependency: "direct main"
+    description:
+      name: sign_in_with_apple
+      sha256: "8bd875c8e8748272749eb6d25b896f768e7e9d60988446d543fe85a37a2392b8"
+      url: "https://pub.dev"
+    source: hosted
+    version: "7.0.1"
+  sign_in_with_apple_platform_interface:
+    dependency: transitive
+    description:
+      name: sign_in_with_apple_platform_interface
+      sha256: "981bca52cf3bb9c3ad7ef44aace2d543e5c468bb713fd8dda4275ff76dfa6659"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.0"
+  sign_in_with_apple_web:
+    dependency: transitive
+    description:
+      name: sign_in_with_apple_web
+      sha256: f316400827f52cafcf50d00e1a2e8a0abc534ca1264e856a81c5f06bd5b10fed
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.0.0"
   sky_engine:
     dependency: transitive
     description: flutter

+ 4 - 1
pubspec.yaml

@@ -3,7 +3,7 @@ description: "A new Flutter project."
 
 publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 
-version: 1.0.1+9
+version: 1.1.1+10
 
 environment:
   sdk: ^3.7.0
@@ -121,6 +121,9 @@ dependencies:
   # uuid生成
   uuid: ^4.2.2
 
+  # Apple登录
+  sign_in_with_apple: ^7.0.1
+
   #android日志打印
   atmob_logging:
     version: ^0.0.5