@@ -19,8 +21,65 @@
@implementation KBWebViewViewController
++ (instancetype)legalViewControllerWithType:(KBLegalDocumentType)type {
+ KBWebViewViewController *vc = [[KBWebViewViewController alloc] init];
+ vc.pageTitle = [self kb_titleForLegalDocumentType:type];
+ NSString *remoteURL = [self kb_remoteURLForLegalDocumentType:type];
+ if (remoteURL.length > 0) {
+ vc.url = remoteURL;
+ } else {
+ vc.htmlString = [self kb_htmlForLegalDocumentType:type];
+ }
+ return vc;
+}
+
++ (void)presentLegalDocumentType:(KBLegalDocumentType)type fromViewController:(UIViewController *)viewController {
+ if (![viewController isKindOfClass:UIViewController.class]) { return; }
+ KBWebViewViewController *vc = [self legalViewControllerWithType:type];
+ UINavigationController *nav = viewController.navigationController;
+ if (nav) {
+ [nav pushViewController:vc animated:YES];
+ return;
+ }
+ [viewController presentViewController:vc animated:YES completion:nil];
+}
+
++ (nullable NSNumber *)legalDocumentTypeNumberFromQueryValue:(NSString *)queryValue {
+ NSString *value = queryValue.lowercaseString ?: @"";
+ if ([value isEqualToString:@"privacy"]) {
+ return @(KBLegalDocumentTypePrivacyPolicy);
+ }
+ if ([value isEqualToString:@"membership"]) {
+ return @(KBLegalDocumentTypeMembershipAgreement);
+ }
+ if ([value isEqualToString:@"terms"]) {
+ return @(KBLegalDocumentTypeTermsOfService);
+ }
+ return nil;
+}
+
++ (NSString *)queryValueForLegalDocumentType:(KBLegalDocumentType)type {
+ switch (type) {
+ case KBLegalDocumentTypePrivacyPolicy:
+ return @"privacy";
+ case KBLegalDocumentTypeMembershipAgreement:
+ return @"membership";
+ case KBLegalDocumentTypeTermsOfService:
+ default:
+ return @"terms";
+ }
+}
+
+- (void)viewWillAppear:(BOOL)animated {
+ [super viewWillAppear:animated];
+ if (self.pageTitle.length > 0) {
+ self.title = self.pageTitle;
+ }
+}
+
- (void)viewDidLoad {
[super viewDidLoad];
+ self.view.backgroundColor = UIColor.whiteColor;
[self configUI];
}
@@ -34,14 +93,15 @@
// 2. 创建 webView
self.webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:config];
self.webView.navigationDelegate = self;
- self.webView.translatesAutoresizingMaskIntoConstraints = NO;
self.webView.backgroundColor = UIColor.clearColor;
- self.webView.frame = self.view.bounds;
[self.view addSubview:self.webView];
+ [self.webView mas_makeConstraints:^(MASConstraintMaker *make) {
+ make.top.equalTo(self.kb_navView.mas_bottom);
+ make.left.right.bottom.equalTo(self.view);
+ }];
// 🟢 3. 顶部 2 像素进度条
self.progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
- self.progressView.translatesAutoresizingMaskIntoConstraints = NO;
self.progressView.trackTintColor = [UIColor clearColor]; // 背景透明
self.progressView.progressTintColor = [UIColor greenColor];
@@ -52,12 +112,11 @@
[self.view addSubview:self.progressView];
// 约束:贴在最上面,高度 2 像素,左右撑满
- [NSLayoutConstraint activateConstraints:@[
- [self.progressView.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
- [self.progressView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
- [self.progressView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
- [self.progressView.heightAnchor constraintEqualToConstant:2.0]
- ]];
+ [self.progressView mas_makeConstraints:^(MASConstraintMaker *make) {
+ make.top.equalTo(self.kb_navView.mas_bottom);
+ make.left.right.equalTo(self.view);
+ make.height.mas_equalTo(2.0);
+ }];
// 🟢 4. 监听 WKWebView 加载进度(estimatedProgress)
[self.webView addObserver:self
@@ -66,9 +125,26 @@
context:nil];
self.observingProgress = YES;
- // 5. 加载 URL
- NSURLRequest * req = [NSURLRequest requestWithURL:[NSURL URLWithString:self.url]];
+ // 5. 加载内容
+ if (self.htmlString.length > 0) {
+ [self.webView loadHTMLString:self.htmlString baseURL:nil];
+ if (self.pageTitle.length > 0) {
+ self.title = self.pageTitle;
+ }
+ return;
+ }
+
+ NSURL *URL = [NSURL URLWithString:self.url ?: @""];
+ if (!URL) {
+ [self.webView loadHTMLString:[self.class kb_fallbackErrorHTML]
+ baseURL:nil];
+ return;
+ }
+
+ NSURLRequest * req = [NSURLRequest requestWithURL:URL];
[self.webView loadRequest:req];
+ [self.view bringSubviewToFront:self.kb_navView];
+ [self.view bringSubviewToFront:self.progressView];
}
#pragma mark - KVO: 监听加载进度(estimatedProgress) 🟢
@@ -115,7 +191,11 @@
// 加载完成
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
NSLog(@"页面加载成功");
- self.title = webView.title;
+ if (self.pageTitle.length > 0) {
+ self.title = self.pageTitle;
+ } else if (webView.title.length > 0) {
+ self.title = webView.title;
+ }
}
// 加载失败
@@ -159,6 +239,55 @@ didFailProvisionalNavigation:(WKNavigation *)navigation
}
}
+#pragma mark - Legal Content
+
++ (NSString *)kb_titleForLegalDocumentType:(KBLegalDocumentType)type {
+ switch (type) {
+ case KBLegalDocumentTypePrivacyPolicy:
+ return KBLocalized(@"Privacy Policy");
+ case KBLegalDocumentTypeMembershipAgreement:
+ return KBLocalized(@"Membership Agreement");
+ case KBLegalDocumentTypeTermsOfService:
+ default:
+ return KBLocalized(@"Agreement");
+ }
+}
+
++ (NSString *)kb_remoteURLForLegalDocumentType:(KBLegalDocumentType)type {
+ switch (type) {
+ case KBLegalDocumentTypePrivacyPolicy:
+ return KB_PRIVACY_POLICY_URL;
+ case KBLegalDocumentTypeMembershipAgreement:
+ return KB_MEMBERSHIP_AGREEMENT_URL;
+ case KBLegalDocumentTypeTermsOfService:
+ default:
+ return KB_TERMS_OF_SERVICE_URL;
+ }
+}
+
++ (NSString *)kb_htmlForLegalDocumentType:(KBLegalDocumentType)type {
+ NSString *title = [self kb_titleForLegalDocumentType:type];
+ NSString *body = @"";
+ switch (type) {
+ case KBLegalDocumentTypePrivacyPolicy:
+ body = @"Overview
This in-app privacy disclosure explains how the app and the custom keyboard handle data when you use account, AI, subscription, sync, and voice features.
Full Access
Network-based features inside the keyboard require Full Access. If you do not enable Full Access, those features stay unavailable.
Data Used For Features You Trigger
When you actively use AI reply, cloud sync, account, purchase verification, or voice input, the content required for that feature may be transmitted to the service provider to complete your request.
This may include typed text you choose to send, voice audio you record, account identifiers, email address, subscription status, and limited diagnostics needed for app functionality and fraud prevention.
Keyboard Boundaries
The custom keyboard does not operate in secure text fields and cannot access content in contexts where iOS blocks third-party keyboards.
Retention And Deletion
Account-related data is retained only as needed for app functionality, purchases, support, and legal compliance. Use the in-app account deletion flow to request account removal and associated cleanup.
Important
Replace this fallback page with your final published privacy policy URL before App Store submission so that the wording exactly matches App Store Connect privacy labels and your backend behavior.
";
+ break;
+ case KBLegalDocumentTypeMembershipAgreement:
+ body = @"Subscription Terms
Paid membership unlocks subscription benefits for eligible premium features. Pricing, billing period, and any trial details are shown on the purchase sheet before you confirm payment.
Auto-Renewal
Subscriptions renew automatically unless cancelled at least 24 hours before the end of the current billing period. Renewal charges are handled by Apple through your App Store account.
Managing Your Subscription
You can restore purchases inside the app and manage or cancel subscriptions in Apple ID subscription settings after purchase.
Feature Availability
Some premium actions started from the custom keyboard may open the main app to complete login, purchase, or subscription management.
Important
Replace this fallback page with your final published membership agreement URL before App Store submission.
";
+ break;
+ case KBLegalDocumentTypeTermsOfService:
+ default:
+ body = @"Service Scope
This app provides a custom keyboard, account features, premium subscriptions, and optional AI-assisted and voice features. Some capabilities require network access and may open the main app to complete the flow.
Acceptable Use
You must not use the service to violate law, harass others, infringe rights, or generate abusive, sexual, hateful, or otherwise prohibited content.
AI And Voice Features
AI-generated or transcribed content may be inaccurate, incomplete, or inappropriate. You remain responsible for reviewing content before sending or relying on it.
Accounts And Purchases
You are responsible for activity performed through your account. Paid features are subject to Apple billing rules and any product limitations shown in the app.
Important
Replace this fallback page with your final published terms URL before App Store submission.
";
+ break;
+ }
+
+ return [NSString stringWithFormat:@"%@
%@Built-in fallback document. Configure the final public URL in KBConfig.h when release content is ready.
", title, body];
+}
+
++ (NSString *)kb_fallbackErrorHTML {
+ return @"Page unavailable
The requested document could not be loaded.
";
+}
+
#pragma mark - Clean up 🟢
- (void)dealloc {
diff --git a/keyBoard/Info.plist b/keyBoard/Info.plist
index 7074000..254abed 100644
--- a/keyBoard/Info.plist
+++ b/keyBoard/Info.plist
@@ -21,7 +21,7 @@