Files
keyboard/keyBoard/Class/WebView/KBWebViewViewController.m

307 lines
14 KiB
Mathematica
Raw Normal View History

2025-11-10 16:09:47 +08:00
//
// KBWebViewViewController.m
// keyBoard
//
// Created by on 2025/11/10.
//
#import "KBWebViewViewController.h"
#import <WebKit/WebKit.h>
2026-03-08 21:29:10 +08:00
#import "KBConfig.h"
#import "Masonry.h"
2025-11-10 16:09:47 +08:00
@interface KBWebViewViewController () <WKNavigationDelegate, WKScriptMessageHandler>
@property(nonatomic, strong) WKWebView * webView;
// 🟢
@property(nonatomic, strong) UIProgressView *progressView;
@property(nonatomic, assign) BOOL observingProgress;
@end
@implementation KBWebViewViewController
2026-03-08 21:29:10 +08:00
+ (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;
}
}
2025-11-10 16:09:47 +08:00
- (void)viewDidLoad {
[super viewDidLoad];
2026-03-08 21:29:10 +08:00
self.view.backgroundColor = UIColor.whiteColor;
2025-11-10 16:09:47 +08:00
[self configUI];
}
- (void)configUI {
// 1. JS
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
[userContentController addScriptMessageHandler:self name:@"keyBoard"];
config.userContentController = userContentController;
// 2. webView
self.webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:config];
self.webView.navigationDelegate = self;
self.webView.backgroundColor = UIColor.clearColor;
[self.view addSubview:self.webView];
2026-03-08 21:29:10 +08:00
[self.webView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.kb_navView.mas_bottom);
make.left.right.bottom.equalTo(self.view);
}];
2025-11-10 16:09:47 +08:00
// 🟢 3. 2
self.progressView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
self.progressView.trackTintColor = [UIColor clearColor]; //
self.progressView.progressTintColor = [UIColor greenColor];
self.progressView.progress = 0.0f;
self.progressView.hidden = YES; //
[self.view addSubview:self.progressView];
// 2
2026-03-08 21:29:10 +08:00
[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);
}];
2025-11-10 16:09:47 +08:00
// 🟢 4. WKWebView estimatedProgress
[self.webView addObserver:self
forKeyPath:@"estimatedProgress"
options:NSKeyValueObservingOptionNew
context:nil];
self.observingProgress = YES;
2026-03-08 21:29:10 +08:00
// 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];
2025-11-10 16:09:47 +08:00
[self.webView loadRequest:req];
2026-03-08 21:29:10 +08:00
[self.view bringSubviewToFront:self.kb_navView];
[self.view bringSubviewToFront:self.progressView];
2025-11-10 16:09:47 +08:00
}
#pragma mark - KVO: estimatedProgress 🟢
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
if (object == self.webView && [keyPath isEqualToString:@"estimatedProgress"]) {
CGFloat progress = self.webView.estimatedProgress;
// 0 ~ 1
self.progressView.hidden = NO;
[self.progressView setProgress:progress animated:YES];
if (progress >= 1.0f) {
//
[UIView animateWithDuration:0.25
delay:0.25
options:UIViewAnimationOptionCurveEaseOut
animations:^{
self.progressView.alpha = 0.0;
} completion:^(BOOL finished) {
self.progressView.progress = 0.0;
self.progressView.hidden = YES;
self.progressView.alpha = 1.0;
}];
}
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark - WKNavigationDelegate
//
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
NSLog(@"开始加载url");
//
self.progressView.hidden = NO;
self.progressView.progress = 0.0;
}
//
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
NSLog(@"页面加载成功");
2026-03-08 21:29:10 +08:00
if (self.pageTitle.length > 0) {
self.title = self.pageTitle;
} else if (webView.title.length > 0) {
self.title = webView.title;
}
2025-11-10 16:09:47 +08:00
}
//
- (void)webView:(WKWebView *)webView
didFailProvisionalNavigation:(WKNavigation *)navigation
withError:(NSError *)error {
NSLog(@"webView load error: %@", error);
//
self.progressView.hidden = YES;
self.progressView.progress = 0.0;
}
#pragma mark - WKScriptMessageHandler JS
- (void)userContentController:(WKUserContentController *)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message {
if (![message.body isKindOfClass:NSDictionary.class]) {
NSLog(@"Body参数不合法");
return;
}
[self handleJSMethodWithParams:message.body];
}
- (void)handleJSMethodWithParams:(NSDictionary *)params {
NSDictionary * body = [params copy];
NSLog(@"收到 JS 消息: %@", body);
NSString * type = body[@"type"];
//
if ([type isEqual:@"ONE_METHOD"]) {
NSLog(@"come on baby js 调用你的方法了");
}
// title
if ([type isEqual:@"changeTiele"]) {
NSLog(@"%@", body);
NSString * newTiele = body[@"payload"][@"data"];
self.title = newTiele;
}
}
2026-03-08 21:29:10 +08:00
#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 = @"<section><h2>Overview</h2><p>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.</p></section><section><h2>Full Access</h2><p>Network-based features inside the keyboard require Full Access. If you do not enable Full Access, those features stay unavailable.</p></section><section><h2>Data Used For Features You Trigger</h2><p>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.</p><p>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.</p></section><section><h2>Keyboard Boundaries</h2><p>The custom keyboard does not operate in secure text fields and cannot access content in contexts where iOS blocks third-party keyboards.</p></section><section><h2>Retention And Deletion</h2><p>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.</p></section><section><h2>Important</h2><p>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.</p></section>";
break;
case KBLegalDocumentTypeMembershipAgreement:
body = @"<section><h2>Subscription Terms</h2><p>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.</p></section><section><h2>Auto-Renewal</h2><p>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.</p></section><section><h2>Managing Your Subscription</h2><p>You can restore purchases inside the app and manage or cancel subscriptions in Apple ID subscription settings after purchase.</p></section><section><h2>Feature Availability</h2><p>Some premium actions started from the custom keyboard may open the main app to complete login, purchase, or subscription management.</p></section><section><h2>Important</h2><p>Replace this fallback page with your final published membership agreement URL before App Store submission.</p></section>";
break;
case KBLegalDocumentTypeTermsOfService:
default:
body = @"<section><h2>Service Scope</h2><p>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.</p></section><section><h2>Acceptable Use</h2><p>You must not use the service to violate law, harass others, infringe rights, or generate abusive, sexual, hateful, or otherwise prohibited content.</p></section><section><h2>AI And Voice Features</h2><p>AI-generated or transcribed content may be inaccurate, incomplete, or inappropriate. You remain responsible for reviewing content before sending or relying on it.</p></section><section><h2>Accounts And Purchases</h2><p>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.</p></section><section><h2>Important</h2><p>Replace this fallback page with your final published terms URL before App Store submission.</p></section>";
break;
}
return [NSString stringWithFormat:@"<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1,maximum-scale=1'><style>body{font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue',sans-serif;margin:0;padding:24px 18px 48px;color:#1f2937;background:#ffffff;line-height:1.6;}h1{font-size:28px;line-height:1.2;margin:0 0 20px;color:#111827;}h2{font-size:18px;line-height:1.3;margin:24px 0 10px;color:#111827;}p{font-size:15px;margin:0 0 10px;color:#4b5563;}section{padding-bottom:4px;border-bottom:1px solid #eef2f7;}section:last-child{border-bottom:none;} .note{margin-top:18px;font-size:13px;color:#6b7280;}</style></head><body><h1>%@</h1>%@<p class='note'>Built-in fallback document. Configure the final public URL in KBConfig.h when release content is ready.</p></body></html>", title, body];
}
+ (NSString *)kb_fallbackErrorHTML {
return @"<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><style>body{font-family:-apple-system,BlinkMacSystemFont,'Helvetica Neue',sans-serif;padding:32px;color:#1f2937;}h1{font-size:22px;margin:0 0 12px;}p{font-size:15px;line-height:1.6;color:#4b5563;}</style></head><body><h1>Page unavailable</h1><p>The requested document could not be loaded.</p></body></html>";
}
2025-11-10 16:09:47 +08:00
#pragma mark - Clean up 🟢
- (void)dealloc {
if (self.observingProgress) {
@try {
[self.webView removeObserver:self forKeyPath:@"estimatedProgress"];
} @catch (NSException *exception) {
NSLog(@"removeObserver estimatedProgress exception: %@", exception);
}
}
// 便 JS
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"keyBoard"];
}
@end