个人主页开发

This commit is contained in:
zeng 2025-12-11 11:33:56 +08:00
parent 3b0495b1ed
commit e14d8e2c6b
68 changed files with 3602 additions and 26 deletions

View File

@ -178,6 +178,41 @@
F3B8593E2EE677740095A9CC /* NRLocalizedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8593D2EE677740095A9CC /* NRLocalizedModel.swift */; };
F3B859402EE6787E0095A9CC /* NRLocalizedManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8593F2EE6787E0095A9CC /* NRLocalizedManager.swift */; };
F3B859422EE678FB0095A9CC /* NRSettingAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859412EE678FB0095A9CC /* NRSettingAPI.swift */; };
F3B859442EE902BF0095A9CC /* NRStoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859432EE902BF0095A9CC /* NRStoreViewController.swift */; };
F3B8594C2EE904980095A9CC /* NRPayDateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8594B2EE904980095A9CC /* NRPayDateModel.swift */; };
F3B8594E2EE905A70095A9CC /* NRStoreAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8594D2EE905A40095A9CC /* NRStoreAPI.swift */; };
F3B859522EE906A90095A9CC /* JXIAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859512EE906A80095A9CC /* JXIAPManager.swift */; };
F3B859572EE9072C0095A9CC /* NRIapManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859562EE907280095A9CC /* NRIapManager.swift */; };
F3B859592EE9073B0095A9CC /* NRIAPOrderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859582EE907350095A9CC /* NRIAPOrderModel.swift */; };
F3B8595B2EE907600095A9CC /* NRWaitRestoreModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8595A2EE907600095A9CC /* NRWaitRestoreModel.swift */; };
F3B8595D2EE907710095A9CC /* NRIAPVerifyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8595C2EE907710095A9CC /* NRIAPVerifyModel.swift */; };
F3B8595F2EE910020095A9CC /* NRPayDataRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8595E2EE910020095A9CC /* NRPayDataRequest.swift */; };
F3B859612EE9126E0095A9CC /* Dictionary+NRAdd.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859602EE9126A0095A9CC /* Dictionary+NRAdd.swift */; };
F3B859632EE91B850095A9CC /* NRStoreCoinsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859622EE91B850095A9CC /* NRStoreCoinsView.swift */; };
F3B859652EE91BB70095A9CC /* NRStoreCoinsBigCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859642EE91BB70095A9CC /* NRStoreCoinsBigCell.swift */; };
F3B859672EE91BC50095A9CC /* NRStoreCoinsSmallCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859662EE91BC50095A9CC /* NRStoreCoinsSmallCell.swift */; };
F3B859692EE91BD70095A9CC /* NRStoreCoinsPackCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859682EE91BD70095A9CC /* NRStoreCoinsPackCell.swift */; };
F3B8596B2EE91C9F0095A9CC /* NRStoreCoinsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8596A2EE91C9F0095A9CC /* NRStoreCoinsCell.swift */; };
F3B8596D2EE944DE0095A9CC /* NRStoreVipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8596C2EE944DE0095A9CC /* NRStoreVipView.swift */; };
F3B8596F2EE9456E0095A9CC /* NRStoreVipCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8596E2EE9456E0095A9CC /* NRStoreVipCell.swift */; };
F3B859712EE94A1B0095A9CC /* NRFeedbackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859702EE94A1B0095A9CC /* NRFeedbackViewController.swift */; };
F3B859732EE94A760095A9CC /* NRAppWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859722EE94A760095A9CC /* NRAppWebViewController.swift */; };
F3B859752EE9515B0095A9CC /* NRMeCoinsPackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859742EE9515B0095A9CC /* NRMeCoinsPackView.swift */; };
F3B859772EE95B220095A9CC /* NRMeVipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859762EE95B220095A9CC /* NRMeVipView.swift */; };
F3B859792EE960D20095A9CC /* NRWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859782EE960D20095A9CC /* NRWalletViewController.swift */; };
F3B8597B2EE961EF0095A9CC /* NRWalletCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8597A2EE961EF0095A9CC /* NRWalletCell.swift */; };
F3B8597D2EE9627B0095A9CC /* NRWalletHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8597C2EE9627B0095A9CC /* NRWalletHeaderView.swift */; };
F3B8597F2EE96F810095A9CC /* NRConsumptionRecordsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8597E2EE96F810095A9CC /* NRConsumptionRecordsViewController.swift */; };
F3B859812EE9716E0095A9CC /* NRBuyRecordsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859802EE9716E0095A9CC /* NRBuyRecordsModel.swift */; };
F3B859862EE972F70095A9CC /* NRConsumptionRecordsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859842EE972F70095A9CC /* NRConsumptionRecordsCell.swift */; };
F3B859872EE972F70095A9CC /* NRConsumptionRecordsCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = F3B859852EE972F70095A9CC /* NRConsumptionRecordsCell.xib */; };
F3B859892EE97E1F0095A9CC /* NRRewardCoinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859882EE97E1F0095A9CC /* NRRewardCoinsViewController.swift */; };
F3B8598B2EEA51540095A9CC /* NRRewardCoinsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8598A2EEA51540095A9CC /* NRRewardCoinsModel.swift */; };
F3B8598D2EEA51FE0095A9CC /* NRRewardCoinsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8598C2EEA51FE0095A9CC /* NRRewardCoinsCell.swift */; };
F3B8598F2EEA5B1C0095A9CC /* NROrderRecordsPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B8598E2EEA5B1C0095A9CC /* NROrderRecordsPageViewController.swift */; };
F3B859912EEA627F0095A9CC /* NROrderRecordsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859902EEA627F0095A9CC /* NROrderRecordsViewController.swift */; };
F3B859932EEA63CD0095A9CC /* NROrderRecordsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859922EEA63CD0095A9CC /* NROrderRecordsModel.swift */; };
F3B859952EEA64430095A9CC /* NROrderRecordsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B859942EEA64430095A9CC /* NROrderRecordsCell.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -358,6 +393,41 @@
F3B8593D2EE677740095A9CC /* NRLocalizedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRLocalizedModel.swift; sourceTree = "<group>"; };
F3B8593F2EE6787E0095A9CC /* NRLocalizedManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRLocalizedManager.swift; sourceTree = "<group>"; };
F3B859412EE678FB0095A9CC /* NRSettingAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRSettingAPI.swift; sourceTree = "<group>"; };
F3B859432EE902BF0095A9CC /* NRStoreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRStoreViewController.swift; sourceTree = "<group>"; };
F3B8594B2EE904980095A9CC /* NRPayDateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRPayDateModel.swift; sourceTree = "<group>"; };
F3B8594D2EE905A40095A9CC /* NRStoreAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRStoreAPI.swift; sourceTree = "<group>"; };
F3B859512EE906A80095A9CC /* JXIAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JXIAPManager.swift; sourceTree = "<group>"; };
F3B859562EE907280095A9CC /* NRIapManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRIapManager.swift; sourceTree = "<group>"; };
F3B859582EE907350095A9CC /* NRIAPOrderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRIAPOrderModel.swift; sourceTree = "<group>"; };
F3B8595A2EE907600095A9CC /* NRWaitRestoreModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRWaitRestoreModel.swift; sourceTree = "<group>"; };
F3B8595C2EE907710095A9CC /* NRIAPVerifyModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRIAPVerifyModel.swift; sourceTree = "<group>"; };
F3B8595E2EE910020095A9CC /* NRPayDataRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRPayDataRequest.swift; sourceTree = "<group>"; };
F3B859602EE9126A0095A9CC /* Dictionary+NRAdd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+NRAdd.swift"; sourceTree = "<group>"; };
F3B859622EE91B850095A9CC /* NRStoreCoinsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRStoreCoinsView.swift; sourceTree = "<group>"; };
F3B859642EE91BB70095A9CC /* NRStoreCoinsBigCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRStoreCoinsBigCell.swift; sourceTree = "<group>"; };
F3B859662EE91BC50095A9CC /* NRStoreCoinsSmallCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRStoreCoinsSmallCell.swift; sourceTree = "<group>"; };
F3B859682EE91BD70095A9CC /* NRStoreCoinsPackCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRStoreCoinsPackCell.swift; sourceTree = "<group>"; };
F3B8596A2EE91C9F0095A9CC /* NRStoreCoinsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRStoreCoinsCell.swift; sourceTree = "<group>"; };
F3B8596C2EE944DE0095A9CC /* NRStoreVipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRStoreVipView.swift; sourceTree = "<group>"; };
F3B8596E2EE9456E0095A9CC /* NRStoreVipCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRStoreVipCell.swift; sourceTree = "<group>"; };
F3B859702EE94A1B0095A9CC /* NRFeedbackViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRFeedbackViewController.swift; sourceTree = "<group>"; };
F3B859722EE94A760095A9CC /* NRAppWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRAppWebViewController.swift; sourceTree = "<group>"; };
F3B859742EE9515B0095A9CC /* NRMeCoinsPackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRMeCoinsPackView.swift; sourceTree = "<group>"; };
F3B859762EE95B220095A9CC /* NRMeVipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRMeVipView.swift; sourceTree = "<group>"; };
F3B859782EE960D20095A9CC /* NRWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRWalletViewController.swift; sourceTree = "<group>"; };
F3B8597A2EE961EF0095A9CC /* NRWalletCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRWalletCell.swift; sourceTree = "<group>"; };
F3B8597C2EE9627B0095A9CC /* NRWalletHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRWalletHeaderView.swift; sourceTree = "<group>"; };
F3B8597E2EE96F810095A9CC /* NRConsumptionRecordsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRConsumptionRecordsViewController.swift; sourceTree = "<group>"; };
F3B859802EE9716E0095A9CC /* NRBuyRecordsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRBuyRecordsModel.swift; sourceTree = "<group>"; };
F3B859842EE972F70095A9CC /* NRConsumptionRecordsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRConsumptionRecordsCell.swift; sourceTree = "<group>"; };
F3B859852EE972F70095A9CC /* NRConsumptionRecordsCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NRConsumptionRecordsCell.xib; sourceTree = "<group>"; };
F3B859882EE97E1F0095A9CC /* NRRewardCoinsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRRewardCoinsViewController.swift; sourceTree = "<group>"; };
F3B8598A2EEA51540095A9CC /* NRRewardCoinsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRRewardCoinsModel.swift; sourceTree = "<group>"; };
F3B8598C2EEA51FE0095A9CC /* NRRewardCoinsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRRewardCoinsCell.swift; sourceTree = "<group>"; };
F3B8598E2EEA5B1C0095A9CC /* NROrderRecordsPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NROrderRecordsPageViewController.swift; sourceTree = "<group>"; };
F3B859902EEA627F0095A9CC /* NROrderRecordsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NROrderRecordsViewController.swift; sourceTree = "<group>"; };
F3B859922EEA63CD0095A9CC /* NROrderRecordsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NROrderRecordsModel.swift; sourceTree = "<group>"; };
F3B859942EEA64430095A9CC /* NROrderRecordsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NROrderRecordsCell.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -375,6 +445,7 @@
0373D93C2ED578FC0017DCC7 /* API */ = {
isa = PBXGroup;
children = (
F3B8594D2EE905A40095A9CC /* NRStoreAPI.swift */,
F34991022EE160E50039E939 /* NRUserAPI.swift */,
F343492B2EDE72EE00AA7E70 /* NRHomeAPI.swift */,
0373D94A2ED582E10017DCC7 /* NRNovelAPI.swift */,
@ -485,6 +556,7 @@
039810752ED054090006E317 /* Class */,
03980F8E2ED00ABC0006E317 /* Source */,
039810742ED053F40006E317 /* Libs */,
F3B8594F2EE9068E0095A9CC /* Thirdparty */,
);
path = ReaderHive;
sourceTree = "<group>";
@ -559,6 +631,7 @@
0398106E2ED053630006E317 /* Extension */ = {
isa = PBXGroup;
children = (
F3B859602EE9126A0095A9CC /* Dictionary+NRAdd.swift */,
F34990B62EDE78680039E939 /* UIScrollView+Refresh.swift */,
F34348E62ED7F91500AA7E70 /* NSNumber+NRAdd.swift */,
0373D95B2ED598830017DCC7 /* UIStackView+NRAdd.swift */,
@ -576,6 +649,7 @@
039810742ED053F40006E317 /* Libs */ = {
isa = PBXGroup;
children = (
F3B859552EE907220095A9CC /* IAP */,
F3B8593A2EE676B30095A9CC /* LocalizedManager */,
F349911D2EE26B020039E939 /* Alert */,
F34990EF2EE00F460039E939 /* KeyedArchiver */,
@ -598,6 +672,7 @@
F34348A92ED5B59C00AA7E70 /* Explore */,
0373D93D2ED57A500017DCC7 /* Novel */,
F34990C02EDFCD180039E939 /* Me */,
F3B859452EE904390095A9CC /* Store */,
);
path = Class;
sourceTree = "<group>";
@ -905,6 +980,8 @@
F34990FE2EE158790039E939 /* NRMeCell.swift */,
F34991002EE1593A0039E939 /* NRMeHeaderView.swift */,
F3B859322EE66DD10095A9CC /* NRMeCoinsContentView.swift */,
F3B859762EE95B220095A9CC /* NRMeVipView.swift */,
F3B859742EE9515B0095A9CC /* NRMeCoinsPackView.swift */,
F3B859342EE66F530095A9CC /* NRMeCoinsView.swift */,
F34991082EE169C60039E939 /* NRAboutHeaderView.swift */,
F349910A2EE16B520039E939 /* NRAboutCell.swift */,
@ -922,6 +999,7 @@
F34991132EE175E30039E939 /* NRHistoryViewController.swift */,
F34991152EE176640039E939 /* NRNovelHistoryViewController.swift */,
F3B859362EE6750B0095A9CC /* NRLanguageViewController.swift */,
F3B859702EE94A1B0095A9CC /* NRFeedbackViewController.swift */,
);
path = VC;
sourceTree = "<group>";
@ -938,6 +1016,7 @@
isa = PBXGroup;
children = (
F349910D2EE1707C0039E939 /* NRWebViewController.swift */,
F3B859722EE94A760095A9CC /* NRAppWebViewController.swift */,
F349910F2EE170850039E939 /* NRWebViewController+Script.swift */,
F34991112EE170DD0039E939 /* NRWebView.swift */,
);
@ -963,6 +1042,88 @@
path = LocalizedManager;
sourceTree = "<group>";
};
F3B859452EE904390095A9CC /* Store */ = {
isa = PBXGroup;
children = (
F3B8594A2EE9045C0095A9CC /* VC */,
F3B859482EE9044F0095A9CC /* V */,
F3B859492EE904580095A9CC /* M */,
);
path = Store;
sourceTree = "<group>";
};
F3B859482EE9044F0095A9CC /* V */ = {
isa = PBXGroup;
children = (
F3B859622EE91B850095A9CC /* NRStoreCoinsView.swift */,
F3B8596C2EE944DE0095A9CC /* NRStoreVipView.swift */,
F3B8596A2EE91C9F0095A9CC /* NRStoreCoinsCell.swift */,
F3B859642EE91BB70095A9CC /* NRStoreCoinsBigCell.swift */,
F3B859662EE91BC50095A9CC /* NRStoreCoinsSmallCell.swift */,
F3B859682EE91BD70095A9CC /* NRStoreCoinsPackCell.swift */,
F3B8596E2EE9456E0095A9CC /* NRStoreVipCell.swift */,
F3B8597A2EE961EF0095A9CC /* NRWalletCell.swift */,
F3B8597C2EE9627B0095A9CC /* NRWalletHeaderView.swift */,
F3B859842EE972F70095A9CC /* NRConsumptionRecordsCell.swift */,
F3B859852EE972F70095A9CC /* NRConsumptionRecordsCell.xib */,
F3B8598C2EEA51FE0095A9CC /* NRRewardCoinsCell.swift */,
F3B859942EEA64430095A9CC /* NROrderRecordsCell.swift */,
);
path = V;
sourceTree = "<group>";
};
F3B859492EE904580095A9CC /* M */ = {
isa = PBXGroup;
children = (
F3B8594B2EE904980095A9CC /* NRPayDateModel.swift */,
F3B859802EE9716E0095A9CC /* NRBuyRecordsModel.swift */,
F3B8598A2EEA51540095A9CC /* NRRewardCoinsModel.swift */,
F3B859922EEA63CD0095A9CC /* NROrderRecordsModel.swift */,
);
path = M;
sourceTree = "<group>";
};
F3B8594A2EE9045C0095A9CC /* VC */ = {
isa = PBXGroup;
children = (
F3B859432EE902BF0095A9CC /* NRStoreViewController.swift */,
F3B859782EE960D20095A9CC /* NRWalletViewController.swift */,
F3B8597E2EE96F810095A9CC /* NRConsumptionRecordsViewController.swift */,
F3B859882EE97E1F0095A9CC /* NRRewardCoinsViewController.swift */,
F3B8598E2EEA5B1C0095A9CC /* NROrderRecordsPageViewController.swift */,
F3B859902EEA627F0095A9CC /* NROrderRecordsViewController.swift */,
);
path = VC;
sourceTree = "<group>";
};
F3B8594F2EE9068E0095A9CC /* Thirdparty */ = {
isa = PBXGroup;
children = (
F3B859502EE906A40095A9CC /* JXIAPManager */,
);
path = Thirdparty;
sourceTree = "<group>";
};
F3B859502EE906A40095A9CC /* JXIAPManager */ = {
isa = PBXGroup;
children = (
F3B859512EE906A80095A9CC /* JXIAPManager.swift */,
);
path = JXIAPManager;
sourceTree = "<group>";
};
F3B859552EE907220095A9CC /* IAP */ = {
isa = PBXGroup;
children = (
F3B859562EE907280095A9CC /* NRIapManager.swift */,
F3B8595E2EE910020095A9CC /* NRPayDataRequest.swift */,
F3B859582EE907350095A9CC /* NRIAPOrderModel.swift */,
F3B8595C2EE907710095A9CC /* NRIAPVerifyModel.swift */,
F3B8595A2EE907600095A9CC /* NRWaitRestoreModel.swift */,
);
path = IAP;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -1027,6 +1188,7 @@
files = (
03980F8A2ED009EB0006E317 /* Assets.xcassets in Resources */,
039810732ED053BE0006E317 /* Localizable.strings in Resources */,
F3B859872EE972F70095A9CC /* NRConsumptionRecordsCell.xib in Resources */,
03980F8C2ED009EB0006E317 /* LaunchScreen.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -1094,10 +1256,15 @@
F34348FF2ED85BF200AA7E70 /* NRReadBatteryView.swift in Sources */,
F3B8593C2EE677170095A9CC /* NRLanguageModel.swift in Sources */,
039810BA2ED4377E0006E317 /* NRHomeNovelReadWhatCell.swift in Sources */,
F3B8596B2EE91C9F0095A9CC /* NRStoreCoinsCell.swift in Sources */,
F3B859712EE94A1B0095A9CC /* NRFeedbackViewController.swift in Sources */,
F34348C72ED6CCBC00AA7E70 /* NRExploreNovelContentListViewController.swift in Sources */,
039810B82ED431780006E317 /* NRHomeNovelReadWhatView.swift in Sources */,
F34349142EDA9AE900AA7E70 /* NRNovelReadSettingView.swift in Sources */,
F3B859632EE91B850095A9CC /* NRStoreCoinsView.swift in Sources */,
F34348BD2ED68F2B00AA7E70 /* NRExploreNovelViewModel.swift in Sources */,
F3B8595F2EE910020095A9CC /* NRPayDataRequest.swift in Sources */,
F3B859692EE91BD70095A9CC /* NRStoreCoinsPackCell.swift in Sources */,
F34991102EE1708C0039E939 /* NRWebViewController+Script.swift in Sources */,
F34348B12ED5B9A400AA7E70 /* NRExploreNovelViewController.swift in Sources */,
F34348BF2ED691C100AA7E70 /* NRExploreNovelGenresViewController.swift in Sources */,
@ -1112,26 +1279,33 @@
F34348C32ED6A20700AA7E70 /* NRExploreNovelContentViewController.swift in Sources */,
F34991292EE285660039E939 /* NRShowRecommendPop.swift in Sources */,
0373D9642ED5ABBC0017DCC7 /* NREmpty.swift in Sources */,
F3B859572EE9072C0095A9CC /* NRIapManager.swift in Sources */,
039810B62ED42D840006E317 /* NRHomeNovelNewArrivalsCell.swift in Sources */,
0373D95A2ED593D50017DCC7 /* NRSearchRecordCell.swift in Sources */,
0373D9582ED5935D0017DCC7 /* NRSearchRecordView.swift in Sources */,
F343490C2ED9751800AA7E70 /* NRReadChapterCatalogModel.swift in Sources */,
F3B859862EE972F70095A9CC /* NRConsumptionRecordsCell.swift in Sources */,
F34990C72EDFCE500039E939 /* NRNovelReadGradeView.swift in Sources */,
039810B42ED428F20006E317 /* NRHomeNovelNewArrivalsView.swift in Sources */,
F34991012EE1593A0039E939 /* NRMeHeaderView.swift in Sources */,
F3B8598F2EEA5B1C0095A9CC /* NROrderRecordsPageViewController.swift in Sources */,
F34991052EE165EA0039E939 /* NRMeItem.swift in Sources */,
039810CC2ED477CD0006E317 /* UIView+NRAdd.swift in Sources */,
F3B859312EE66B950095A9CC /* NRDetailRechargeView.swift in Sources */,
F34348B92ED5C7E400AA7E70 /* NRExploreNovelMenuCell.swift in Sources */,
F343490A2ED96EE600AA7E70 /* NRNovelReadView.swift in Sources */,
039810B22ED3F5FB0006E317 /* NRGradientView.swift in Sources */,
F3B8596D2EE944DE0095A9CC /* NRStoreVipView.swift in Sources */,
039810852ED056D70006E317 /* UserDefaults+NRAdd.swift in Sources */,
F3B8595B2EE907600095A9CC /* NRWaitRestoreModel.swift in Sources */,
039810CA2ED469D50006E317 /* NRWaterfallFlowLayout.swift in Sources */,
F34348F72ED84B0D00AA7E70 /* NRNovelReaderViewController+Page.swift in Sources */,
039810902ED060EF0006E317 /* NRViewController.swift in Sources */,
039810702ED053910006E317 /* String+NRAdd.swift in Sources */,
039810932ED062CE0006E317 /* NRHomeViewController.swift in Sources */,
F3B859652EE91BB70095A9CC /* NRStoreCoinsBigCell.swift in Sources */,
039810BC2ED43C8E0006E317 /* NRReadWhatViewTransformer.swift in Sources */,
F3B859932EEA63CD0095A9CC /* NROrderRecordsModel.swift in Sources */,
039810D02ED54D370006E317 /* NRHomeCategoryTagView.swift in Sources */,
F34348E32ED70D2F00AA7E70 /* NRNovelDetailHeaderView+Data.swift in Sources */,
0398107F2ED055D10006E317 /* NRLoginManager.swift in Sources */,
@ -1148,6 +1322,7 @@
0373D9472ED57F3F0017DCC7 /* UINavigationBar+NRAdd.swift in Sources */,
0373D94D2ED583A80017DCC7 /* NRNovelModel.swift in Sources */,
F34348AF2ED5B85300AA7E70 /* NRExploreViewController.swift in Sources */,
F3B859732EE94A760095A9CC /* NRAppWebViewController.swift in Sources */,
0373D9602ED59DA10017DCC7 /* NRSearchResultCell.swift in Sources */,
039810952ED066710006E317 /* UIScreen+NRAdd.swift in Sources */,
039810662ED04F940006E317 /* NRUrlPath.swift in Sources */,
@ -1155,16 +1330,20 @@
F34991232EE26EAC0039E939 /* NRAlert.swift in Sources */,
F34348B72ED5C75800AA7E70 /* NRTableViewCell.swift in Sources */,
F34349102ED9A77A00AA7E70 /* NRPanModalContentView.swift in Sources */,
F3B859892EE97E1F0095A9CC /* NRRewardCoinsViewController.swift in Sources */,
039810CE2ED47A130006E317 /* CGMutablePath+NRRoundedCorner.swift in Sources */,
F34348E72ED7F91C00AA7E70 /* NSNumber+NRAdd.swift in Sources */,
F3B8597F2EE96F810095A9CC /* NRConsumptionRecordsViewController.swift in Sources */,
F34349122EDA84F100AA7E70 /* NRProgressView.swift in Sources */,
F349910B2EE16B520039E939 /* NRAboutCell.swift in Sources */,
F3B859592EE9073B0095A9CC /* NRIAPOrderModel.swift in Sources */,
039810A22ED070400006E317 /* NRHomeNovelHeaderView.swift in Sources */,
F34349082ED945DA00AA7E70 /* NRCoreText.swift in Sources */,
F349911F2EE26C350039E939 /* NRAlertWindowManager.swift in Sources */,
F34991072EE167E80039E939 /* NRAboutViewController.swift in Sources */,
F34348ED2ED82B6300AA7E70 /* NRNovelReadSetManager.swift in Sources */,
F34990FF2EE158790039E939 /* NRMeCell.swift in Sources */,
F3B859752EE9515B0095A9CC /* NRMeCoinsPackView.swift in Sources */,
F34348DD2ED6F9F900AA7E70 /* NRNovelDetailMoreLikeCell.swift in Sources */,
039810AC2ED3EF640006E317 /* NRHomeNovelHeaderContentView.swift in Sources */,
0398106D2ED053000006E317 /* NRDefine.swift in Sources */,
@ -1173,16 +1352,21 @@
F343492C2EDE72F300AA7E70 /* NRHomeAPI.swift in Sources */,
F3B859372EE6750B0095A9CC /* NRLanguageViewController.swift in Sources */,
F34348E12ED70A2700AA7E70 /* NRNovelDetailHeaderView+NovelCoverInfo.swift in Sources */,
F3B859522EE906A90095A9CC /* JXIAPManager.swift in Sources */,
F34349012ED93A9B00AA7E70 /* NRReadChapterModel.swift in Sources */,
F34348DF2ED7049E00AA7E70 /* NRNovelDetailHeaderView.swift in Sources */,
F34348BB2ED5CD8100AA7E70 /* NRExploreNovelMenuItem.swift in Sources */,
0398109B2ED0692A0006E317 /* NRImageView.swift in Sources */,
0398108A2ED0582F0006E317 /* NRDeviceId.swift in Sources */,
F3B859812EE9716E0095A9CC /* NRBuyRecordsModel.swift in Sources */,
F3B859952EEA64430095A9CC /* NROrderRecordsCell.swift in Sources */,
0373D9542ED58AF00017DCC7 /* NRSearchInputView.swift in Sources */,
F34348EB2ED82B4100AA7E70 /* NRNovelReadSet.swift in Sources */,
039810682ED050390006E317 /* NRResponseCryptor.swift in Sources */,
039810C62ED45AE30006E317 /* NRHomeNovelHotTagCell.swift in Sources */,
F3B859912EEA627F0095A9CC /* NROrderRecordsViewController.swift in Sources */,
F34348D92ED6F01900AA7E70 /* NRNovelDetailBottomView.swift in Sources */,
F3B8597B2EE961EF0095A9CC /* NRWalletCell.swift in Sources */,
F34348CB2ED6DADA00AA7E70 /* NRNovelGenresViewController.swift in Sources */,
F3B859422EE678FB0095A9CC /* NRSettingAPI.swift in Sources */,
0373D9522ED58A950017DCC7 /* NRSearchViewModel.swift in Sources */,
@ -1200,14 +1384,18 @@
0373D9562ED5933F0017DCC7 /* NRSearchHomeView.swift in Sources */,
F3B859352EE66F530095A9CC /* NRMeCoinsView.swift in Sources */,
0398108C2ED0584F0006E317 /* NRKeychain.swift in Sources */,
F3B859792EE960D20095A9CC /* NRWalletViewController.swift in Sources */,
039810A02ED06B7C0006E317 /* NRHomeNovelViewController.swift in Sources */,
F34990FB2EE121490039E939 /* NRLabel.swift in Sources */,
F3B859332EE66DD10095A9CC /* NRMeCoinsContentView.swift in Sources */,
F34348F22ED8388F00AA7E70 /* NRNovelReadTopView.swift in Sources */,
F3B859612EE9126E0095A9CC /* Dictionary+NRAdd.swift in Sources */,
F34349182EDAA02900AA7E70 /* NRNovelReadSettingItemView.swift in Sources */,
039810812ED056090006E317 /* NRLoginToken.swift in Sources */,
F3B859442EE902BF0095A9CC /* NRStoreViewController.swift in Sources */,
F343491A2EDAC2E500AA7E70 /* NRReadSettingThemeView.swift in Sources */,
0373D9402ED57B1C0017DCC7 /* NRNovelDetailViewController.swift in Sources */,
F3B8598B2EEA51540095A9CC /* NRRewardCoinsModel.swift in Sources */,
039810A62ED072820006E317 /* NRCollectionView.swift in Sources */,
039810602ED04D950006E317 /* NRNetworkModel.swift in Sources */,
F34348C92ED6CDD900AA7E70 /* NRExploreNovelContentListCell.swift in Sources */,
@ -1221,23 +1409,30 @@
039810A82ED3E7DB0006E317 /* NRHomeNovelListCell.swift in Sources */,
F34991212EE26C660039E939 /* NRBaseAlert.swift in Sources */,
F34990FD2EE124CF0039E939 /* NRNovelReadStarGradeView.swift in Sources */,
F3B8594C2EE904980095A9CC /* NRPayDateModel.swift in Sources */,
F34991182EE1780A0039E939 /* NRNovelHistoryCell.swift in Sources */,
F34990F12EE00F5A0039E939 /* NRKeyedArchiver.swift in Sources */,
F34349162EDA9FC700AA7E70 /* NRReadSettingBrightnessView.swift in Sources */,
F3B8598D2EEA51FE0095A9CC /* NRRewardCoinsCell.swift in Sources */,
F34990F52EE0346B0039E939 /* NRNovelReadBaseViewController.swift in Sources */,
039810642ED04F480006E317 /* NRTargetType.swift in Sources */,
F3B8595D2EE907710095A9CC /* NRIAPVerifyModel.swift in Sources */,
F343491E2EDAD0AA00AA7E70 /* NRReadTheme.swift in Sources */,
F3B8597D2EE9627B0095A9CC /* NRWalletHeaderView.swift in Sources */,
039810872ED057260006E317 /* NRUserDefaultsKey.swift in Sources */,
F34349222EDD227A00AA7E70 /* NRReadSettingSpacingView.swift in Sources */,
F34990F32EE02FD60039E939 /* NRNovelReadFinishViewController.swift in Sources */,
F34348F92ED855AA00AA7E70 /* NRNovelReadContentViewController.swift in Sources */,
039810A42ED072380006E317 /* NRHomeNovelListViewController.swift in Sources */,
039810BE2ED44C210006E317 /* NRHomeNovelHotGridView.swift in Sources */,
F3B859672EE91BC50095A9CC /* NRStoreCoinsSmallCell.swift in Sources */,
F3B859392EE676610095A9CC /* NRLanguageCell.swift in Sources */,
0373D95E2ED59C430017DCC7 /* NRSearchResultView.swift in Sources */,
F34991142EE175E30039E939 /* NRHistoryViewController.swift in Sources */,
F3B8593E2EE677740095A9CC /* NRLocalizedModel.swift in Sources */,
039810AA2ED3EC2E0006E317 /* NRScrollView.swift in Sources */,
F3B859772EE95B220095A9CC /* NRMeVipView.swift in Sources */,
F3B8594E2EE905A70095A9CC /* NRStoreAPI.swift in Sources */,
0373D94F2ED58A1E0017DCC7 /* NRSearchViewController.swift in Sources */,
0398108E2ED060020006E317 /* UIFont+NRAdd.swift in Sources */,
F34348D32ED6E0F400AA7E70 /* NRMyListViewController.swift in Sources */,
@ -1247,6 +1442,7 @@
F34348D72ED6E7C600AA7E70 /* NRMyListNovelCell.swift in Sources */,
F34349052ED9442300AA7E70 /* NRReadPageModel.swift in Sources */,
F34990F92EE118FC0039E939 /* NRNovelReadFinishHeaderView.swift in Sources */,
F3B8596F2EE9456E0095A9CC /* NRStoreVipCell.swift in Sources */,
F34990C52EDFCD4A0039E939 /* NRMeViewController.swift in Sources */,
F349911C2EE190590039E939 /* NRNovelDetailCatalogViewController.swift in Sources */,
F34348F02ED8381E00AA7E70 /* NRNovelReadViewModel.swift in Sources */,
@ -1282,13 +1478,15 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 8NNUR9HPV3;
DEVELOPMENT_TEAM = "";
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ReaderHive/Source/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ReaderHive;
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = "";
@ -1300,8 +1498,9 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.hbqinjiu.ReaderHive;
PRODUCT_BUNDLE_IDENTIFIER = com.lssj.ReaderHive;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
@ -1320,12 +1519,16 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 8NNUR9HPV3;
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 9JR2Y32ZU3;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ReaderHive/Source/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ReaderHive;
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = "";
@ -1337,8 +1540,10 @@
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.hbqinjiu.ReaderHive;
PRODUCT_BUNDLE_IDENTIFIER = com.lssj.ReaderHive;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = readerdev;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;

View File

@ -6,6 +6,11 @@
//
import UIKit
///
let kNROsVersion: String = UIDevice.current.systemVersion
let kNRAPPBundleIdentifier: String = (Bundle.main.infoDictionary!["CFBundleIdentifier"] as? String) ?? "0"
///app
let kNRAPPVersion: String = (Bundle.main.infoDictionary!["CFBundleShortVersionString"] as? String) ?? "0"
let kNRAPPBundleVersion: String = (Bundle.main.infoDictionary!["CFBundleVersion"] as? String) ?? "0"

View File

@ -11,3 +11,5 @@ let kNRLoginTokenDefaultsKey = "kNRLoginTokenDefaultsKey"
let kNRUserInfoDefaultsKey = "kNRUserInfoDefaultsKey"
///
let kNRNovelReadSetDefaultsKey = "kNRNovelReadSetDefaultsKey"
let kNRWaitRestoreIAPDefaultsKey = "kNRWaitRestoreIAPDefaultsKey"

View File

@ -0,0 +1,23 @@
//
// Dictionary+NRAdd.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
extension Dictionary {
func toJsonString() -> String? {
do {
let data = try JSONSerialization.data(withJSONObject: self)
let jsonStr = String(data: data, encoding: .utf8)
return jsonStr
} catch {
}
return nil
}
}

View File

@ -0,0 +1,130 @@
//
// NRStoreAPI.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import SmartCodable
import Alamofire
struct NRStoreAPI {
enum BuyType: String, SmartCaseDefaultable {
case coins = "coins"
case subVip = "sub_vip"
case subCoins = "sub_coins"
}
///
static func requestPayTemplate(isLoding: Bool = false, isToast: Bool = true, completer: ((_ model: NRPayDateModel?) -> Void)?) {
var param = NRNetwork.Parameters(path: "/paySettingsV4")
param.method = .get
param.isToast = isToast
param.isLoding = isLoding
param.parameters = [
"discount" : "1",
"purchases_token" : JXIAPManager.manager.getAppStoreReceipt() ?? "",
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRPayDateModel>) in
if response.isSuccess {
completer?(response.data)
} else {
completer?(nil)
}
}
}
///
static func requestCreateOrder(payId: String, shortPlayId: String, videoId: String, isDiscount: Bool = false, identifierDiscount: String? = nil, completer: ((_ orderModel: NRIAPOrderModel?) -> Void)?) {
var param = NRNetwork.Parameters(path: "/createOrder")
param.isToast = false
param.parameters = [
"payment_channel" : "apple",
"short_play_id" : shortPlayId,
"video_id" : videoId,
"pay_setting_id" : payId,
"is_discount" : isDiscount ? 1 : 0,
"product_discount" : identifierDiscount ?? "",
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRIAPOrderModel>) in
guard let data = response.data else {
NRToast.show(text: "network_error_2".localized)
completer?(nil)
return
}
if let message = data.message, message.count > 0 {
if response.data?.code == 30007 {
NRToast.show(text: "pay_error_1".localized)
} else {
NRToast.show(text: message)
}
completer?(nil)
} else {
completer?(data)
}
}
}
///
static func requestVerifyOrder(parameters: [String : Any], completer: ((_ response: NRNetwork.Response<NRIAPVerifyModel>) -> Void)?) {
var param = NRNetwork.Parameters(path: "/applePaid")
param.parameters = parameters
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRIAPVerifyModel>) in
completer?(response)
}
}
static func requestBuyRecords(page: Int, completer: ((_ listModel: NRNetwork.List<NRBuyRecordsModel>?) -> Void)?) {
var param = NRNetwork.Parameters(path: "/getCustomerBuyRecords")
param.method = .get
param.parameters = [
"page_size" : 20,
"current_page" : page,
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNetwork.List<NRBuyRecordsModel>>) in
completer?(response.data)
}
}
static func requestRechargeRecord(page: Int, buyType: BuyType, completer: ((_ listModel: NRNetwork.List<NROrderRecordsModel>?) -> Void)?) {
var param = NRNetwork.Parameters(path: "/getCustomerOrder")
param.method = .get
param.parameters = [
"page_size" : 20,
"current_page" : page,
"buy_type" : buyType.rawValue
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNetwork.List<NROrderRecordsModel>>) in
completer?(response.data)
}
}
static func reuqestSendCoinRecord(page: Int, completer: ((_ listModel: NRNetwork.List<NRRewardCoinsModel>?) -> Void)?) {
var param = NRNetwork.Parameters(path: "/sendCoinList")
param.method = .post
param.parameters = [
"page_size" : 20,
"current_page" : page,
]
NRNetwork.request(parameters: param) { (response: NRNetwork.Response<NRNetwork.List<NRRewardCoinsModel>>) in
completer?(response.data)
}
}
}

View File

@ -57,7 +57,7 @@ extension NRTargetType: TargetType {
"model" : UIDevice.current.machineModelName ?? "",
"authorization" : NRLoginManager.manager.token?.token ?? "",
"device-gaid" : UIDevice.current.identifierForVendor?.uuidString ?? "",
"product-prefix" : "ReaderHive"
"product-prefix" : NRIapManager.IAPPrefix
]
#if DEBUG
dic["security"] = "false"

View File

@ -11,6 +11,7 @@ let NRBaseURL = "https://api-readerhive.readerhive.net"
let NRWebBaseURL = "https://www.readerhive.net"
let NRCampaignWebURL = "https://campaign.readerhive.com"
@ -18,3 +19,15 @@ let NRWebBaseURL = "https://www.readerhive.net"
let kNRUserAgreementWebUrl = NRWebBaseURL + "/user_policy"
///
let kNRPrivacyPolicyWebUrl = NRWebBaseURL + "/private"
///
let kNRFeedBackHomeWebUrl = NRCampaignWebURL + "/pages/leave/index"
///
let kNRFeedBackListWebUrl = NRCampaignWebURL + "/pages/leave/list"
///
let kNRFeedBackDetailWebUrl = NRCampaignWebURL + "/pages/leave/detail"
///
let kNRLogoutWebUrl = NRCampaignWebURL + "/pages/setting/logout"

View File

@ -16,15 +16,12 @@ class NRTableViewCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.layer.rasterizationScale = UIScreen.main.scale
self.layer.shouldRasterize = true
self.selectionStyle = .none
self.backgroundColor = .clear
_init()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
super.init(coder: coder)
_init()
}
override func awakeFromNib() {
@ -37,6 +34,13 @@ class NRTableViewCell: UITableViewCell {
// Configure the view for the selected state
}
private func _init() {
self.layer.rasterizationScale = UIScreen.main.scale
self.layer.shouldRasterize = true
self.selectionStyle = .none
self.backgroundColor = .clear
}
}

View File

@ -0,0 +1,78 @@
//
// NRAppWebViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
internal import WebKit
class NRAppWebViewController: NRWebViewController {
var id: String?
private var receiveDataCount = 0
var theme: String? = "theme_3"
override func viewDidLoad() {
super.viewDidLoad()
if webUrl == kNRFeedBackListWebUrl {
self.title = "Feedback History".localized
} else if webUrl == kNRFeedBackHomeWebUrl {
self.title = "Feedback".localized
} else if webUrl == kNRFeedBackDetailWebUrl {
self.title = "Feedback Details".localized
} else if webUrl == kNRLogoutWebUrl {
self.title = "Account Deletion".localized
}
}
override func nr_webViewDidFinishLoad(_ webView: NRWebView) {
super.nr_webViewDidFinishLoad(webView)
receiveDataCount = 0
receiveDataFromNative()
}
}
extension NRAppWebViewController {
func receiveDataFromNative() {
receiveDataCount += 1
if receiveDataCount > 10 { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
var dic = [
"token" : NRLoginManager.manager.token?.token ?? "",
"time_zone" : NRTargetType.timeZone(),
"lang" : NRLocalizedManager.shared.currentLocalizedKey,
"type" : "ios",
"device-id" : NRDeviceId.shared.id
]
if let theme = theme {
dic["theme"] = theme
}
if let id = id {
dic["id"] = id
}
if let json = dic.toJsonString() {
let js = "receiveDataFromNative(\(json))"
self.webView.evaluateJavaScript(js) { [weak self] _, error in
guard let self = self else { return }
if error != nil {
self.receiveDataFromNative()
}
}
}
}
}
}

View File

@ -6,7 +6,7 @@
//
import UIKit
@preconcurrency import WebKit
internal import WebKit
import YYText
//MARK:-------------- VPWebViewDelegate --------------

View File

@ -6,7 +6,7 @@
//
import UIKit
import WebKit
internal import WebKit
///APP
let kNRWebMessageAPP = "js2app"

View File

@ -6,7 +6,7 @@
//
import UIKit
import WebKit
internal import WebKit
import SnapKit
class NRWebViewController: NRViewController {

View File

@ -15,6 +15,10 @@ struct NRMeItem {
case about
case settings
case web
case feedback
case consumptionRecords
case purchaseRecords
case rewardCoins
}
var type: ItemType

View File

@ -7,6 +7,7 @@
import UIKit
import SnapKit
import YYCategories
class NRMeCoinsContentView: UIView {
@ -24,12 +25,22 @@ class NRMeCoinsContentView: UIView {
private lazy var coinsView: NRMeCoinsView = {
let view = NRMeCoinsView()
view.title = "Coins".localized
view.addGestureRecognizer(UITapGestureRecognizer(actionBlock: { [weak self] _ in
guard let self = self else { return }
let vc = NRWalletViewController()
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}))
return view
}()
private lazy var sendCoinsView: NRMeCoinsView = {
let view = NRMeCoinsView()
view.title = "Bonus".localized
view.addGestureRecognizer(UITapGestureRecognizer(actionBlock: { [weak self] _ in
guard let self = self else { return }
let vc = NRWalletViewController()
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}))
return view
}()
@ -40,7 +51,11 @@ class NRMeCoinsContentView: UIView {
}()
private lazy var topUpButton: UIButton = {
let button = NRGradientButton(type: .custom)
let button = NRGradientButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
let vc = NRStoreViewController()
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}))
button.colors = [UIColor.F_3912_F.cgColor, UIColor.FF_4_A_4_A.cgColor, UIColor.FA_9_B_1_F.cgColor]
button.startPoint = .init(x: 0, y: 0.5)
button.endPoint = .init(x: 1, y: 0.5)

View File

@ -0,0 +1,95 @@
//
// NRMeCoinsPackView.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import SnapKit
import YYCategories
class NRMeCoinsPackView: UIView {
private lazy var bgView: UIImageView = {
let view = UIImageView(image: UIImage(named: "me_pack_bg_image"))
view.layer.cornerRadius = 30
view.layer.masksToBounds = true
view.layer.borderWidth = 1
view.layer.borderColor = UIColor.FFEFD_4.cgColor
return view
}()
private lazy var iconImageView = UIImageView(image: UIImage(named: "gift_icon_01"))
private lazy var titleLabel: UILabel = {
let label = NRLabel()
label.font = .font(ofSize: 14, weight: .semibold)
label.textColors = [UIColor.F_3912_F.cgColor, UIColor.FF_4_A_4_A.cgColor, UIColor.FA_9_B_1_F.cgColor]
label.textStartPoint = .init(x: 0, y: 0.5)
label.textEndPoint = .init(x: 1, y: 0.5)
label.text = "me_coins_pack_title".localized
return label
}()
private lazy var subtitleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular).withItalic()
label.textColor = .F_9710_D
label.text = "me_coins_pack_subtitle".localized
return label
}()
private lazy var indicatorImageView = UIImageView(image: UIImage(named: "arrow_right_icon_07"))
override init(frame: CGRect) {
super.init(frame: frame)
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRMeCoinsPackView {
private func nr_setupUI() {
addSubview(bgView)
addSubview(iconImageView)
addSubview(titleLabel)
addSubview(subtitleLabel)
addSubview(indicatorImageView)
bgView.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.height.equalTo(60)
}
iconImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(24)
}
titleLabel.snp.makeConstraints { make in
make.left.equalTo(iconImageView.snp.right).offset(8)
make.top.equalToSuperview().offset(12)
make.right.lessThanOrEqualToSuperview().offset(-48)
}
subtitleLabel.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.bottom.equalToSuperview().offset(-12)
make.right.lessThanOrEqualToSuperview().offset(-48)
}
indicatorImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-16)
}
}
}

View File

@ -45,7 +45,6 @@ class NRMeHeaderView: UITableViewHeaderFooterView {
private lazy var copyButton: UIButton = {
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
UIPasteboard.general.string = NRLoginManager.manager.userInfo?.customer_id
NRToast.show(text: "Success")
}))
@ -65,6 +64,16 @@ class NRMeHeaderView: UITableViewHeaderFooterView {
return view
}()
private lazy var coinsPackView: NRMeCoinsPackView = {
let view = NRMeCoinsPackView()
return view
}()
private lazy var vipView: NRMeVipView = {
let view = NRMeVipView()
return view
}()
deinit {
NotificationCenter.default.removeObserver(self)
}
@ -72,12 +81,13 @@ class NRMeHeaderView: UITableViewHeaderFooterView {
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
NotificationCenter.default.addObserver(self, selector: #selector(userInfoUpdateNotification), name: NRLoginManager.userInfoUpdateNotification, object: nil)
nr_setupUI()
userInfoUpdateNotification()
stackView.addArrangedSubview(coinsView)
stackView.addArrangedSubview(vipView)
stackView.addArrangedSubview(coinsPackView)
}
@ -105,13 +115,13 @@ extension NRMeHeaderView {
contentView.addSubview(idBgView)
idBgView.addSubview(idLabel)
idBgView.addSubview(copyButton)
// contentView.addSubview(stackView)
contentView.addSubview(stackView)
avatarImageView.snp.makeConstraints { make in
make.left.equalToSuperview()
make.top.equalToSuperview().offset(48)
make.width.height.equalTo(60)
make.bottom.equalToSuperview().offset(-16)
// make.bottom.equalToSuperview().offset(-16)
}
nickLabel.snp.makeConstraints { make in
@ -136,11 +146,11 @@ extension NRMeHeaderView {
make.left.equalTo(idLabel.snp.right).offset(8)
}
// stackView.snp.makeConstraints { make in
// make.left.right.equalToSuperview()
// make.top.equalTo(avatarImageView.snp.bottom).offset(16)
// make.bottom.equalToSuperview().offset(-16)
// }
stackView.snp.makeConstraints { make in
make.left.right.equalToSuperview()
make.top.equalTo(avatarImageView.snp.bottom).offset(16)
make.bottom.equalToSuperview().offset(-16)
}
}
}

View File

@ -0,0 +1,92 @@
//
// NRMeVipView.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import SnapKit
class NRMeVipView: UIView {
private lazy var contentView: UIView = {
let view = UIView()
view.backgroundColor = .F_2_EFEE
view.layer.cornerRadius = 10
view.layer.masksToBounds = true
return view
}()
private lazy var titleLabel: NRLabel = {
let label = NRLabel()
label.font = .font(ofSize: 12, weight: .semibold)
label.textColors = [UIColor.F_3912_F.cgColor, UIColor.FF_4_A_4_A.cgColor, UIColor.FA_9_B_1_F.cgColor]
label.textStartPoint = .init(x: 0, y: 0.5)
label.textEndPoint = .init(x: 1, y: 0.5)
label.setContentHuggingPriority(.required, for: .horizontal)
label.setContentCompressionResistancePriority(.required, for: .horizontal)
return label
}()
private lazy var iconImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "time_icon_01"))
imageView.setContentHuggingPriority(.required, for: .horizontal)
imageView.setContentCompressionResistancePriority(.required, for: .horizontal)
return imageView
}()
private lazy var timeLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .black.withAlphaComponent(0.5)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
titleLabel.text = "Weekly VIP".localized
timeLabel.text = "Expiration Time2023-11-23"
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRMeVipView {
private func nr_setupUI() {
addSubview(contentView)
contentView.addSubview(titleLabel)
contentView.addSubview(iconImageView)
contentView.addSubview(timeLabel)
contentView.snp.makeConstraints { make in
make.top.bottom.equalToSuperview()
make.height.equalTo(20)
make.centerX.equalToSuperview()
make.right.lessThanOrEqualToSuperview()
}
titleLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(8)
}
iconImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalTo(titleLabel.snp.right).offset(8)
}
timeLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalTo(iconImageView.snp.right).offset(4)
make.right.equalToSuperview().offset(-8)
}
}
}

View File

@ -0,0 +1,30 @@
//
// NRFeedbackViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
class NRFeedbackViewController: NRAppWebViewController {
override func viewDidLoad() {
self.webUrl = kNRFeedBackHomeWebUrl
super.viewDidLoad()
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}

View File

@ -14,7 +14,8 @@ class NRMeViewController: NRViewController {
let arr = [
NRMeItem(type: .history, icon: UIImage(named: "history_icon_01"), title: "History".localized),
// NRMeItem(type: .language, icon: UIImage(named: "language_icon_01"), title: "Language".localized),
NRMeItem(type: .about, icon: UIImage(named: "about_icon_01"), title: "About".localized)
NRMeItem(type: .about, icon: UIImage(named: "about_icon_01"), title: "About".localized),
NRMeItem(type: .feedback, icon: UIImage(named: "feedback_icon_01"), title: "Feedback".localized)
]
return arr
}()
@ -101,6 +102,10 @@ extension NRMeViewController: UITableViewDelegate, UITableViewDataSource {
let vc = NRLanguageViewController()
self.navigationController?.pushViewController(vc, animated: true)
case .feedback:
let vc = NRFeedbackViewController()
self.navigationController?.pushViewController(vc, animated: true)
default:
break
}

View File

@ -0,0 +1,23 @@
//
// NRBuyRecordsModel.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import SmartCodable
class NRBuyRecordsModel: NSObject, SmartCodable {
required override init() { }
var created_at: String?
var short_play_id: String?
var coins: Int?
var short_play_video_id: String?
var coin_type: Int?
var image_url: String?
var name: String?
var episode: String?
}

View File

@ -0,0 +1,18 @@
//
// NROrderRecordsModel.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/11.
//
import UIKit
import SmartCodable
class NROrderRecordsModel: NSObject, SmartCodable {
required override init() { }
var type: String?
var value: String?
var created_at: String?
}

View File

@ -0,0 +1,156 @@
//
// NRPayDateModel.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import SmartCodable
import StoreKit
class NRPayDateModel: NSObject, SmartCodable {
enum SortName: String, SmartCaseDefaultable {
case coin = "list_coins"
case vip = "list_sub_vip"
}
required override init() { }
var list_coins: [NRPayItem]?
var list_sub_vip: [NRPayItem]?
var list_sub_coins: [NRPayItem]?
var sort: [SortName]?
///0: 1
var pay_mode: Int?
///0: 1
var show_type: Int?
}
class NRPayItem: NSObject, SmartCodable {
enum VipTypeKey: String, SmartCaseDefaultable {
case week = "week"
case month = "month"
case quarter = "quarter"
case year = "year"
}
enum SizeType: String, SmartCaseDefaultable {
case big = "big"
case small = "small"
}
required override init() { }
var id: String?
var status: String?
var price: String?
var origin_price: String?
var backhaul_price: String?
var coins: Int?
var send_coins: Int?
var buy_type: NRStoreAPI.BuyType?
var vip_type: String?
var vip_type_key: VipTypeKey?
var sort: String?
var nr_description: String?
var brief: String?
var title: String?
var send_coin_ttl: Int?
var size: SizeType?
var ios_template_id: String?
///
var corner_marker: String?
///
var platform: String?
///
var currency: String?
var ext_info: NRPayExtInfo?
///0 1 2
var discount_type: Int?
@IgnoredKey
var product: SKProduct?
///
var introductionaryOffer: SKProductDiscount? {
return product?.introductoryPrice
}
///
var promotionalOffers: [SKProductDiscount]? {
return product?.discounts
}
static func mappingForKey() -> [SmartKeyTransformer]? {
return [
CodingKeys.nr_description <--- ["description"]
]
}
func getTimeString() -> String? {
switch self.vip_type_key {
case .week:
return "week".localized
case .month:
return "month".localized
case .quarter:
return "quarter".localized
case .year:
return "year".localized
default:
return nil
}
}
func getVipTitle() -> String? {
switch self.vip_type_key {
case .week:
return "Weekly VIP".localized
case .month:
return "Monthly VIP".localized
case .quarter:
return "Quarterly VIP".localized
case .year:
return "Yearly VIP".localized
default:
return nil
}
}
}
class NRPayExtInfo: NSObject, SmartCodable {
required override init() { }
var receive_coins_rate: String?
var max_total_coins: Int?
var extra_day_coins: Int?
var sub_coins_txt_list: [String]?
}

View File

@ -0,0 +1,23 @@
//
// NRRewardCoinsModel.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/11.
//
import UIKit
import SmartCodable
class NRRewardCoinsModel: NSObject, SmartCodable {
required override init() { }
var id: String?
var left_coins: String?
var expired_time: TimeInterval?
var is_effective: Int?
var created_at: String?
var type: String?
var coins: Int?
var diff_datetime: String?
}

View File

@ -0,0 +1,41 @@
//
// NRConsumptionRecordsCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
class NRConsumptionRecordsCell: NRTableViewCell {
var model: NRBuyRecordsModel? {
didSet {
titleLabel.text = "Purchase Single Episode".localized
subtitleLabel.text = "Ch.##".localizedReplace(text: "\(model?.episode ?? "")") + " " + "\(model?.name ?? "")"
timeLabel.text = model?.created_at
coinsLabel.text = "-\(model?.coins ?? 0)" + "Coins".localized
}
}
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var subtitleLabel: UILabel!
@IBOutlet weak var timeLabel: UILabel!
@IBOutlet weak var coinsLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="78" id="mEn-bQ-axV" customClass="NRConsumptionRecordsCell" customModule="ReaderHive" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="415" height="78"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="mEn-bQ-axV" id="X0I-th-PNb">
<rect key="frame" x="0.0" y="0.0" width="415" height="78"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="opT-kU-oJu">
<rect key="frame" x="28" y="16" width="36" height="17"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="14"/>
<color key="textColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="DEl-sR-gIq">
<rect key="frame" x="28" y="47" width="31" height="15"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" name="#999999"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZrS-ca-7Sl">
<rect key="frame" x="356" y="17.666666666666668" width="31" height="14"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<color key="textColor" name="#999999"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="1000" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="1q9-82-w0e">
<rect key="frame" x="351" y="46" width="36" height="17"/>
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="14"/>
<color key="textColor" name="#F9710D"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="1q9-82-w0e" secondAttribute="trailing" constant="28" id="5o1-UO-s6F"/>
<constraint firstItem="ZrS-ca-7Sl" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="opT-kU-oJu" secondAttribute="trailing" constant="30" id="7pK-zi-HYk"/>
<constraint firstItem="1q9-82-w0e" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="DEl-sR-gIq" secondAttribute="trailing" constant="60" id="Dpw-QY-iFP"/>
<constraint firstAttribute="bottom" secondItem="DEl-sR-gIq" secondAttribute="bottom" constant="16" id="IxU-Af-tWS"/>
<constraint firstItem="DEl-sR-gIq" firstAttribute="leading" secondItem="X0I-th-PNb" secondAttribute="leading" constant="28" id="O8d-8Q-5SU"/>
<constraint firstItem="ZrS-ca-7Sl" firstAttribute="centerY" secondItem="opT-kU-oJu" secondAttribute="centerY" id="RXQ-Uw-07h"/>
<constraint firstItem="opT-kU-oJu" firstAttribute="top" secondItem="X0I-th-PNb" secondAttribute="top" constant="16" id="aK0-PR-3qy"/>
<constraint firstItem="opT-kU-oJu" firstAttribute="leading" secondItem="X0I-th-PNb" secondAttribute="leading" constant="28" id="at7-xM-Ouk"/>
<constraint firstAttribute="trailing" secondItem="ZrS-ca-7Sl" secondAttribute="trailing" constant="28" id="cj8-ih-wWV"/>
<constraint firstItem="1q9-82-w0e" firstAttribute="centerY" secondItem="DEl-sR-gIq" secondAttribute="centerY" id="xIx-uT-BHf"/>
</constraints>
</tableViewCellContentView>
<viewLayoutGuide key="safeArea" id="SI6-uw-Qde"/>
<connections>
<outlet property="coinsLabel" destination="1q9-82-w0e" id="Txn-S2-YmX"/>
<outlet property="subtitleLabel" destination="DEl-sR-gIq" id="9KO-QD-Mwx"/>
<outlet property="timeLabel" destination="ZrS-ca-7Sl" id="dKo-E4-twa"/>
<outlet property="titleLabel" destination="opT-kU-oJu" id="HAF-dh-KMA"/>
</connections>
<point key="canvasLocation" x="571.75572519083971" y="53.521126760563384"/>
</tableViewCell>
</objects>
<resources>
<namedColor name="#999999">
<color red="0.59999999999999998" green="0.59999999999999998" blue="0.59999999999999998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="#F9710D">
<color red="0.97647058823529409" green="0.44313725490196076" blue="0.050980392156862744" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@ -0,0 +1,68 @@
//
// NROrderRecordsCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/11.
//
import UIKit
import SnapKit
class NROrderRecordsCell: NRTableViewCell {
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .black
return label
}()
lazy var timeLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = ._999999
return label
}()
lazy var countLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .F_9710_D
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
nr_setupUI()
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NROrderRecordsCell {
private func nr_setupUI() {
contentView.addSubview(titleLabel)
contentView.addSubview(timeLabel)
contentView.addSubview(countLabel)
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(28)
make.top.equalToSuperview().offset(16)
}
timeLabel.snp.makeConstraints { make in
make.left.equalTo(titleLabel)
make.bottom.equalToSuperview().offset(-16)
}
countLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-28)
}
}
}

View File

@ -0,0 +1,119 @@
//
// NRRewardCoinsCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/11.
//
import UIKit
import SnapKit
class NRRewardCoinsCell: NRTableViewCell {
var model: NRRewardCoinsModel? {
didSet {
timeLabel.text = model?.created_at
nameLabel.text = model?.type
countLabel.text = "+\(model?.coins ?? 0)"
remainingLabel.text = model?.left_coins
if model?.is_effective == 1 {
expiresLabel.text = "Expires in ## days".localizedReplace(text: model?.diff_datetime ?? "")
} else {
expiresLabel.text = "Expired".localized
}
}
}
private lazy var timeLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = ._999999
return label
}()
private lazy var nameLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .medium)
label.textColor = .black
return label
}()
private lazy var countLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .F_9710_D
return label
}()
private lazy var remainingLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = ._999999
return label
}()
private lazy var expiresIconImageView = UIImageView(image: UIImage(named: "time_icon_02"))
private lazy var expiresLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .F_9710_D
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRRewardCoinsCell {
private func nr_setupUI() {
contentView.addSubview(timeLabel)
contentView.addSubview(nameLabel)
contentView.addSubview(countLabel)
contentView.addSubview(remainingLabel)
contentView.addSubview(expiresIconImageView)
contentView.addSubview(expiresLabel)
timeLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(28)
make.top.equalToSuperview().offset(16)
}
nameLabel.snp.makeConstraints { make in
make.left.equalTo(timeLabel)
make.centerY.equalToSuperview()
}
expiresIconImageView.snp.makeConstraints { make in
make.left.equalTo(timeLabel)
make.bottom.equalToSuperview().offset(-16)
}
expiresLabel.snp.makeConstraints { make in
make.centerY.equalTo(expiresIconImageView)
make.left.equalTo(expiresIconImageView.snp.right).offset(4)
}
countLabel.snp.makeConstraints { make in
make.centerY.equalTo(nameLabel)
make.right.equalToSuperview().offset(-28)
}
remainingLabel.snp.makeConstraints { make in
make.centerY.equalTo(expiresIconImageView)
make.right.equalTo(countLabel)
}
}
}

View File

@ -0,0 +1,12 @@
//
// NRStoreCoinsBigCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
class NRStoreCoinsBigCell: NRStoreCoinsCell {
}

View File

@ -0,0 +1,19 @@
//
// NRStoreCoinsCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
class NRStoreCoinsCell: UICollectionViewCell {
var model: NRPayItem? {
didSet {
}
}
var nr_isSelected: Bool = false
}

View File

@ -0,0 +1,12 @@
//
// NRStoreCoinsPackCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
class NRStoreCoinsPackCell: NRStoreCoinsCell {
}

View File

@ -0,0 +1,12 @@
//
// NRStoreCoinsSmallCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
class NRStoreCoinsSmallCell: NRStoreCoinsCell {
}

View File

@ -0,0 +1,253 @@
//
// NRStoreCoinsView.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import SnapKit
class NRStoreCoinsView: UIView {
var shortPlayId: String?
var videoId: String?
var buyFinishHandle: (() -> Void)?
var isShowTitle = false {
didSet {
updateLayout()
}
}
private lazy var dataArr: [[NRPayItem]] = []
private var selectedIndexPath: IndexPath?
private lazy var collectionViewLayout: UICollectionViewCompositionalLayout = {
let config = UICollectionViewCompositionalLayoutConfiguration()
config.interSectionSpacing = 10
let layout = UICollectionViewCompositionalLayout { [weak self] section, _ in
guard let self = self else { return nil}
guard let model = dataArr[section].first else { return nil }
if model.buy_type == .subCoins {
return self.coinsBigLayoutSection()
} else if model.size == .big {
return self.bigLayoutSection()
} else {
return self.smallLayoutSection()
}
}
layout.configuration = config
return layout
}()
private lazy var collectionView: NRCollectionView = {
let collectionView = NRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.clipsToBounds = false
collectionView.isScrollEnabled = false
collectionView.register(NRStoreCoinsBigCell.self, forCellWithReuseIdentifier: "NRStoreCoinsBigCell")
collectionView.register(NRStoreCoinsSmallCell.self, forCellWithReuseIdentifier: "NRStoreCoinsSmallCell")
collectionView.register(NRStoreCoinsPackCell.self, forCellWithReuseIdentifier: "NRStoreCoinsPackCell")
collectionView.addObserver(self, forKeyPath: "contentSize", context: nil)
return collectionView
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 16, weight: .semibold)
label.textColor = .black
label.text = "Coins Purchase".localized
return label
}()
deinit {
collectionView.removeObserver(self, forKeyPath: "contentSize")
}
override init(frame: CGRect) {
super.init(frame: frame)
nr_setupUI()
updateLayout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentSize" {
updateLayout()
}
}
func setDataArr(_ arr: [NRPayItem]) {
self.dataArr.removeAll()
var bigArr: [NRPayItem] = []
var smallArr: [NRPayItem] = []
var coinPackArr: [NRPayItem] = []
arr.forEach {
if $0.buy_type == .subCoins {
coinPackArr.append($0)
} else if $0.size == .big {
bigArr.append($0)
} else {
smallArr.append($0)
}
}
if bigArr.count > 0 {
self.dataArr.append(bigArr)
}
if coinPackArr.count > 0 {
self.dataArr.append(coinPackArr)
}
if smallArr.count > 0 {
self.dataArr.append(smallArr)
}
self.collectionView.reloadData()
}
private func updateLayout() {
titleLabel.isHidden = !self.isShowTitle
let height = self.collectionView.contentSize.height + 1
if self.isShowTitle {
self.collectionView.snp.remakeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(12)
make.left.right.bottom.equalToSuperview()
make.height.equalTo(height)
}
} else {
self.collectionView.snp.remakeConstraints { make in
make.top.equalToSuperview().offset(8)
make.left.right.bottom.equalToSuperview()
make.height.equalTo(height)
}
}
}
}
extension NRStoreCoinsView {
private func nr_setupUI() {
addSubview(titleLabel)
addSubview(collectionView)
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.top.equalToSuperview()
}
}
}
extension NRStoreCoinsView {
private func bigLayoutSection() -> NSCollectionLayoutSection {
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(100)), subitems: [item])
group.interItemSpacing = .fixed(1)
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = 10
layoutSection.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
return layoutSection
}
private func smallLayoutSection() -> NSCollectionLayoutSection {
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1 / 3), heightDimension: .fractionalHeight(1)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(121)), subitems: [item])
group.interItemSpacing = .fixed(8)
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = 10
layoutSection.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
return layoutSection
}
private func coinsBigLayoutSection() -> NSCollectionLayoutSection {
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(84)), subitems: [item])
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = 10
layoutSection.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
return layoutSection
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension NRStoreCoinsView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let model = dataArr[indexPath.section][indexPath.row]
var identifier = "NRStoreCoinsBigCell"
if model.buy_type == .subCoins {
identifier = "NRStoreCoinsPackCell"
} else if model.size == .small {
identifier = "NRStoreCoinsSmallCell"
}
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath) as! NRStoreCoinsCell
cell.model = model
cell.nr_isSelected = selectedIndexPath == indexPath
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataArr[section].count
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return dataArr.count
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = dataArr[indexPath.section][indexPath.row]
self.selectedIndexPath = indexPath
collectionView.reloadData()
if model.buy_type == .subCoins {
// let view = FACoinPackConfirmView()
// view.shortPlayId = self.shortPlayId
// view.videoId = self.videoId
// view.model = model
// view.buyFinishHandle = { [weak self] in
// guard let self = self else { return }
// NRLoginManager.manager.updateUserInfo()
// self.buyFinishHandle?()
// }
// view.present(in: nil)
} else {
NRIapManager.manager.start(model: model, shortPlayId: self.shortPlayId, videoId: self.videoId) { [weak self] finish in
guard let self = self else { return }
if finish {
Task {
await NRLoginManager.manager.updateUserInfo()
}
self.buyFinishHandle?()
}
}
}
}
}

View File

@ -0,0 +1,16 @@
//
// NRStoreVipCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
class NRStoreVipCell: UICollectionViewCell {
var model: NRPayItem? {
didSet {
}
}
}

View File

@ -0,0 +1,166 @@
//
// NRStoreVipView.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import SnapKit
class NRStoreVipView: UIView {
var dataArr: [NRPayItem] = [] {
didSet {
collectionView.reloadData()
}
}
var shortPlayId: String?
var videoId: String?
var buyFinishHandle: (() -> Void)?
var isShowTitle = false {
didSet {
updateLayout()
}
}
private lazy var collectionViewLayout: UICollectionViewCompositionalLayout = {
let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1)))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(110)), subitems: [item])
let layoutSection = NSCollectionLayoutSection(group: group)
layoutSection.interGroupSpacing = 14
layoutSection.contentInsets = .init(top: 0, leading: 16, bottom: 0, trailing: 16)
let config = UICollectionViewCompositionalLayoutConfiguration()
let layout = UICollectionViewCompositionalLayout(section: layoutSection)
layout.configuration = config
return layout
}()
private lazy var collectionView: NRCollectionView = {
let collectionView = NRCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.isScrollEnabled = false
collectionView.addObserver(self, forKeyPath: "contentSize", context: nil)
collectionView.register(NRStoreVipCell.self, forCellWithReuseIdentifier: "cell")
return collectionView
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 16, weight: .semibold)
label.textColor = .black
label.text = "VIP Membership".localized
return label
}()
private lazy var subtitleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .regular)
label.textColor = .black.withAlphaComponent(0.25)
label.text = "Auto renew,cancel anytime".localized
return label
}()
deinit {
collectionView.removeObserver(self, forKeyPath: "contentSize")
}
override init(frame: CGRect) {
super.init(frame: frame)
nr_setupUI()
updateLayout()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "contentSize" {
self.updateLayout()
}
}
private func updateLayout() {
titleLabel.isHidden = !self.isShowTitle
subtitleLabel.isHidden = !self.isShowTitle
let height = self.collectionView.contentSize.height + 1
if self.isShowTitle {
self.collectionView.snp.remakeConstraints { make in
make.top.equalTo(subtitleLabel.snp.bottom).offset(12)
make.left.right.bottom.equalToSuperview()
make.height.equalTo(height)
}
} else {
self.collectionView.snp.remakeConstraints { make in
make.top.equalToSuperview().offset(8)
make.left.right.bottom.equalToSuperview()
make.height.equalTo(height)
}
}
}
}
extension NRStoreVipView {
private func nr_setupUI() {
addSubview(titleLabel)
addSubview(subtitleLabel)
addSubview(collectionView)
titleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.top.equalToSuperview()
}
subtitleLabel.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.top.equalTo(titleLabel.snp.bottom).offset(5)
}
}
}
//MARK: UICollectionViewDelegate UICollectionViewDataSource
extension NRStoreVipView: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! NRStoreVipCell
cell.model = self.dataArr[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataArr.count
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = self.dataArr[indexPath.row]
NRIapManager.manager.start(model: model, shortPlayId: self.shortPlayId, videoId: self.videoId) { [weak self] finish in
guard let self = self else { return }
if finish {
Task {
await NRLoginManager.manager.updateUserInfo()
}
self.buyFinishHandle?()
}
}
}
}

View File

@ -0,0 +1,49 @@
//
// NRWalletCell.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import SnapKit
class NRWalletCell: NRTableViewCell {
var item: NRMeItem? {
didSet {
titleLabel.text = item?.title
}
}
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 14, weight: .medium)
label.textColor = .black
return label
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
nr_indicatorImageView.image = UIImage(named: "arrow_right_icon_06")
contentView.addSubview(titleLabel)
contentView.addSubview(nr_indicatorImageView)
titleLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(16)
}
nr_indicatorImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.right.equalToSuperview().offset(-16)
}
}
@MainActor required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,186 @@
//
// NRWalletHeaderView.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import SnapKit
import YYCategories
class NRWalletHeaderView: UITableViewHeaderFooterView {
var userInfo: NRUserInfo? {
didSet {
coinsView.count = userInfo?.coin_left_total
bonusCoinsView.count = userInfo?.send_coin_left_total
}
}
private lazy var bgView: UIView = {
let view = UIView()
view.backgroundColor = .F_2_EFEE
view.layer.cornerRadius = 12
view.layer.masksToBounds = true
return view
}()
private lazy var storeButton: UIButton = {
let button = NRGradientButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
guard let self = self else { return }
let vc = NRStoreViewController()
self.viewController?.navigationController?.pushViewController(vc, animated: true)
}))
button.colors = [UIColor.F_3912_F.cgColor, UIColor.FF_4_A_4_A.cgColor, UIColor.FA_9_B_1_F.cgColor]
button.startPoint = .init(x: 0, y: 0.5)
button.endPoint = .init(x: 1, y: 0.5)
button.layer.cornerRadius = 24
button.layer.masksToBounds = true
button.setTitle("Store".localized, for: .normal)
button.setTitleColor(.white, for: .normal)
button.titleLabel?.font = .font(ofSize: 14, weight: .medium)
return button
}()
private lazy var coinsView: CoinsView = {
let view = CoinsView()
view.title = "Coins".localized
return view
}()
private lazy var bonusCoinsView: CoinsView = {
let view = CoinsView()
view.title = "Bonus".localized
return view
}()
override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier)
nr_setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NRWalletHeaderView {
private func nr_setupUI() {
contentView.addSubview(bgView)
bgView.addSubview(storeButton)
bgView.addSubview(coinsView)
bgView.addSubview(bonusCoinsView)
bgView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.right.equalToSuperview().offset(-16)
make.top.equalToSuperview().offset(24)
make.bottom.equalToSuperview().offset(-12)
make.height.equalTo(144)
}
storeButton.snp.makeConstraints { make in
make.left.equalToSuperview().offset(12)
make.right.equalToSuperview().offset(-12)
make.bottom.equalToSuperview().offset(-16)
make.height.equalTo(48)
}
coinsView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(12)
make.top.equalToSuperview().offset(24)
}
bonusCoinsView.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-12)
make.top.equalTo(coinsView)
make.width.equalTo(coinsView)
make.left.equalTo(coinsView.snp.right).offset(0)
}
}
}
extension NRWalletHeaderView {
class CoinsView: UIView {
var title: String? {
didSet {
titleLabel.text = title
}
}
var count: Int? {
didSet {
coinsLabel.text = "\(count ?? 0)"
}
}
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 12, weight: .semibold)
label.textColor = .black.withAlphaComponent(0.5)
return label
}()
lazy var coinsBgView: UIView = {
let view = UIView()
return view
}()
lazy var coinsImageView = UIImageView(image: UIImage(named: "coins_icon_01"))
lazy var coinsLabel: UILabel = {
let label = UILabel()
label.font = .font(ofSize: 18, weight: .regular)
label.textColor = .F_9710_D
label.text = "0"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(titleLabel)
addSubview(coinsBgView)
coinsBgView.addSubview(coinsImageView)
coinsBgView.addSubview(coinsLabel)
titleLabel.snp.makeConstraints { make in
make.top.equalToSuperview()
make.centerX.equalToSuperview()
make.right.lessThanOrEqualToSuperview()
}
coinsBgView.snp.makeConstraints { make in
make.bottom.equalToSuperview()
make.centerX.equalToSuperview()
make.right.lessThanOrEqualToSuperview()
make.top.equalTo(titleLabel.snp.bottom).offset(10)
}
coinsImageView.snp.makeConstraints { make in
make.top.bottom.equalToSuperview()
make.left.equalToSuperview()
}
coinsLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalTo(coinsImageView.snp.right).offset(4)
make.right.equalToSuperview()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
}

View File

@ -0,0 +1,117 @@
//
// NRConsumptionRecordsViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import SnapKit
import LYEmptyView
class NRConsumptionRecordsViewController: NRViewController {
private lazy var dataArr: [NRBuyRecordsModel] = []
private lazy var page = 1
private lazy var tableView: NRTableView = {
let tableView = NRTableView(frame: .zero, style: .plain)
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = 74
tableView.separatorColor = .F_2_EFEE
tableView.separatorInset = .init(top: 0, left: 16, bottom: 0, right: 16)
tableView.contentInset = .init(top: 16, left: 0, bottom: UIScreen.safeBottom + 10, right: 0)
tableView.register(UINib(nibName: "NRConsumptionRecordsCell", bundle: nil), forCellReuseIdentifier: "cell")
tableView.ly_emptyView = NREmpty.nr_emptyView()
tableView.nr_addRefreshHeader(insetTop: tableView.contentInset.top) { [weak self] in
self?.handleHeaderRefresh(nil)
}
tableView.nr_addRefreshFooter(insetBottom: tableView.contentInset.bottom) { [weak self] in
self?.handleFooterRefresh(nil)
}
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Consumption Records".localized
self.backgroundImageView.isHidden = true
configNavigationBack("arrow_left_icon_05")
nr_setupUI()
requestDataArr(page: 1, completer: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.nr_setNavigationStyle(titleColor: UINavigationBar.titleBlackColor)
}
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
requestDataArr(page: 1) { [weak self] in
self?.tableView.nr_endHeaderRefreshing()
}
}
override func handleFooterRefresh(_ completer: (() -> Void)?) {
requestDataArr(page: self.page + 1) { [weak self] in
self?.tableView.nr_endFooterRefreshing()
}
}
}
extension NRConsumptionRecordsViewController {
private func nr_setupUI() {
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(UIScreen.navBarHeight)
}
}
}
//MARK: UITableViewDelegate UITableViewDataSource
extension NRConsumptionRecordsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! NRConsumptionRecordsCell
cell.model = dataArr[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArr.count
}
}
extension NRConsumptionRecordsViewController {
private func requestDataArr(page: Int, completer: (() -> Void)?) {
NRStoreAPI.requestBuyRecords(page: page) { [weak self] listModel in
guard let self = self else { return }
guard let listModel = listModel, let list = listModel.list else {
completer?()
return
}
if page == 1 {
self.dataArr.removeAll()
}
self.dataArr += list
self.page = page
self.tableView.reloadData()
completer?()
}
}
}

View File

@ -0,0 +1,118 @@
//
// NROrderRecordsViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/11.
//
import UIKit
import JXSegmentedView
import SnapKit
class NROrderRecordsPageViewController: NRViewController {
private lazy var titles = ["Coin Record".localized, "VIP Record".localized]
private lazy var viewControllers: [NROrderRecordsViewController] = {
let vc1 = NROrderRecordsViewController()
vc1.type = .coins
let vc2 = NROrderRecordsViewController()
vc2.type = .subVip
return [vc1, vc2]
}()
private lazy var segmentedDataSource: JXSegmentedTitleDataSource = {
let dataSource = JXSegmentedTitleDataSource()
dataSource.itemWidth = (UIScreen.width - 32 - 18) / 2
dataSource.titles = titles
dataSource.isTitleMaskEnabled = true
dataSource.titleNormalColor = .black.withAlphaComponent(0.5)
dataSource.titleSelectedColor = .white
dataSource.titleNormalFont = .font(ofSize: 14, weight: .medium)
dataSource.titleSelectedFont = .font(ofSize: 14, weight: .medium)
dataSource.itemSpacing = 0
return dataSource
}()
private lazy var segmentedIndicator: JXSegmentedIndicatorBackgroundView = {
let indicator = JXSegmentedIndicatorBackgroundView()
indicator.indicatorHeight = 36
indicator.indicatorWidthIncrement = 0
indicator.indicatorColor = .F_9710_D
return indicator
}()
private lazy var segmentedView: JXSegmentedView = {
let view = JXSegmentedView()
view.backgroundColor = .black.withAlphaComponent(0.05)
view.layer.cornerRadius = 24
view.layer.masksToBounds = true
view.dataSource = segmentedDataSource
view.delegate = self
view.indicators = [segmentedIndicator]
view.listContainer = listContainerView
return view
}()
private lazy var listContainerView: JXSegmentedListContainerView = {
let view = JXSegmentedListContainerView(dataSource: self)
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Order Records".localized
self.backgroundImageView.isHidden = true
configNavigationBack("arrow_left_icon_05")
nr_setupUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.nr_setNavigationStyle(titleColor: UINavigationBar.titleBlackColor)
}
}
extension NROrderRecordsPageViewController {
private func nr_setupUI() {
view.addSubview(segmentedView)
view.addSubview(listContainerView)
segmentedView.snp.makeConstraints { make in
make.left.equalToSuperview().offset(16)
make.centerX.equalToSuperview()
make.top.equalToSuperview().offset(16 + UIScreen.navBarHeight)
make.height.equalTo(48)
}
listContainerView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalTo(segmentedView.snp.bottom)
}
}
}
//MARK: JXSegmentedViewDelegate
extension NROrderRecordsPageViewController: JXSegmentedViewDelegate {
func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) {
}
}
//MARK: JXSegmentedViewDelegate
extension NROrderRecordsPageViewController: JXSegmentedListContainerViewDataSource {
func listContainerView(_ listContainerView: JXSegmentedListContainerView, initListAt index: Int) -> any JXSegmentedListContainerViewListDelegate {
return viewControllers[index]
}
func numberOfLists(in listContainerView: JXSegmentedListContainerView) -> Int {
return self.titles.count
}
}

View File

@ -0,0 +1,108 @@
//
// NROrderRecordsViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/11.
//
import UIKit
import LYEmptyView
import SnapKit
class NROrderRecordsViewController: NRViewController {
var type: NRStoreAPI.BuyType = .coins
private lazy var dataArr: [NROrderRecordsModel] = []
private lazy var page = 1
private lazy var tableView: NRTableView = {
let tableView = NRTableView(frame: .zero, style: .plain)
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = 74
tableView.separatorColor = .F_2_EFEE
tableView.separatorInset = .init(top: 0, left: 16, bottom: 0, right: 16)
tableView.contentInset = .init(top: 6, left: 0, bottom: UIScreen.safeBottom + 10, right: 0)
tableView.register(NROrderRecordsCell.self, forCellReuseIdentifier: "cell")
tableView.ly_emptyView = NREmpty.nr_emptyView()
tableView.nr_addRefreshHeader(insetTop: tableView.contentInset.top) { [weak self] in
self?.handleHeaderRefresh(nil)
}
tableView.nr_addRefreshFooter(insetBottom: tableView.contentInset.bottom) { [weak self] in
self?.handleFooterRefresh(nil)
}
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.backgroundImageView.isHidden = true
self.view.backgroundColor = .clear
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(10)
}
requestDataArr(page: 1, completer: nil)
}
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
requestDataArr(page: 1) { [weak self] in
self?.tableView.nr_endHeaderRefreshing()
}
}
override func handleFooterRefresh(_ completer: (() -> Void)?) {
requestDataArr(page: self.page + 1) { [weak self] in
self?.tableView.nr_endFooterRefreshing()
}
}
}
//MARK: UITableViewDelegate UITableViewDataSource
extension NROrderRecordsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = self.dataArr[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! NROrderRecordsCell
cell.titleLabel.text = model.type
cell.timeLabel.text = model.created_at
cell.countLabel.text = "+\(model.value ?? "0")"
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dataArr.count
}
}
extension NROrderRecordsViewController {
private func requestDataArr(page: Int, completer: (() -> Void)?) {
NRStoreAPI.requestRechargeRecord(page: page, buyType: self.type) { [weak self] listModel in
guard let self = self else { return }
guard let listModel = listModel, let list = listModel.list else {
completer?()
return
}
if page == 1 {
self.dataArr.removeAll()
}
self.dataArr += list
self.page = page
self.tableView.reloadData()
completer?()
}
}
}

View File

@ -0,0 +1,107 @@
//
// NRRewardCoinsViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import LYEmptyView
import SnapKit
class NRRewardCoinsViewController: NRViewController {
private lazy var dataArr: [NRRewardCoinsModel] = []
private lazy var page = 1
private lazy var tableView: NRTableView = {
let tableView = NRTableView(frame: .zero, style: .plain)
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = 98
tableView.separatorColor = .F_2_EFEE
tableView.separatorInset = .init(top: 0, left: 16, bottom: 0, right: 16)
tableView.contentInset = .init(top: 16, left: 0, bottom: UIScreen.safeBottom + 10, right: 0)
tableView.register(NRRewardCoinsCell.self, forCellReuseIdentifier: "cell")
tableView.ly_emptyView = NREmpty.nr_emptyView()
tableView.nr_addRefreshHeader(insetTop: tableView.contentInset.top) { [weak self] in
self?.handleHeaderRefresh(nil)
}
tableView.nr_addRefreshFooter(insetBottom: tableView.contentInset.bottom) { [weak self] in
self?.handleFooterRefresh(nil)
}
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Reward Coins".localized
self.backgroundImageView.isHidden = true
configNavigationBack("arrow_left_icon_05")
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(UIScreen.navBarHeight)
}
requestDataArr(page: 1, completer: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.nr_setNavigationStyle(titleColor: UINavigationBar.titleBlackColor)
}
override func handleHeaderRefresh(_ completer: (() -> Void)?) {
requestDataArr(page: 1) { [weak self] in
self?.tableView.nr_endHeaderRefreshing()
}
}
override func handleFooterRefresh(_ completer: (() -> Void)?) {
requestDataArr(page: self.page + 1) { [weak self] in
self?.tableView.nr_endFooterRefreshing()
}
}
}
//MARK: UITableViewDelegate UITableViewDataSource
extension NRRewardCoinsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! NRRewardCoinsCell
cell.model = self.dataArr[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dataArr.count
}
}
extension NRRewardCoinsViewController {
private func requestDataArr(page: Int, completer: (() -> Void)?) {
NRStoreAPI.reuqestSendCoinRecord(page: page) { [weak self] listModel in
guard let self = self else { return }
guard let listModel = listModel, let list = listModel.list else {
completer?()
return
}
if page == 1 {
self.dataArr.removeAll()
}
self.dataArr += list
self.page = page
self.tableView.reloadData()
completer?()
}
}
}

View File

@ -0,0 +1,82 @@
//
// NRStoreViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import SnapKit
class NRStoreViewController: NRViewController {
private lazy var scrollView: NRScrollView = {
let scrollView = NRScrollView()
return scrollView
}()
private lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = 12
return stackView
}()
private lazy var coinsView: NRStoreCoinsView = {
let view = NRStoreCoinsView()
view.isShowTitle = true
view.buyFinishHandle = { [weak self] in
}
return view
}()
private lazy var vipView: NRStoreVipView = {
let view = NRStoreVipView()
view.isShowTitle = true
view.buyFinishHandle = { [weak self] in
}
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Store".localized
configNavigationBack("arrow_left_icon_05")
stackView.addArrangedSubview(coinsView)
stackView.addArrangedSubview(vipView)
nr_setupUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.nr_setNavigationStyle(titleColor: UINavigationBar.titleBlackColor)
}
}
extension NRStoreViewController {
private func nr_setupUI() {
view.addSubview(scrollView)
scrollView.addSubview(stackView)
scrollView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(UIScreen.navBarHeight)
}
stackView.snp.makeConstraints { make in
make.top.equalToSuperview().offset(12)
make.left.equalToSuperview()
make.centerX.equalToSuperview()
make.bottom.equalToSuperview().offset(-(UIScreen.safeBottom + 10))
}
}
}

View File

@ -0,0 +1,114 @@
//
// NRWalletViewController.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import SnapKit
class NRWalletViewController: NRViewController {
private lazy var dataArr: [NRMeItem] = {
let arr = [
NRMeItem(type: .consumptionRecords, title: "Consumption Records".localized),
NRMeItem(type: .purchaseRecords, title: "Purchase Records".localized),
NRMeItem(type: .rewardCoins, title: "Reward Coins".localized),
]
return arr
}()
private lazy var tableView: NRTableView = {
let tableView = NRTableView(frame: .zero, style: .grouped)
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = 60
tableView.separatorStyle = .none
tableView.contentInset = .init(top: 0, left: 0, bottom: UIScreen.safeBottom + 10, right: 0)
tableView.register(NRWalletCell.self, forCellReuseIdentifier: "cell")
tableView.register(NRWalletHeaderView.self, forHeaderFooterViewReuseIdentifier: "header")
return tableView
}()
deinit {
NotificationCenter.default.removeObserver(self)
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = "My Wallet".localized
self.backgroundImageView.isHidden = true
configNavigationBack("arrow_left_icon_05")
NotificationCenter.default.addObserver(self, selector: #selector(userInfoUpdateNotification), name: NRLoginManager.userInfoUpdateNotification, object: nil)
nr_setupUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: true)
self.nr_setNavigationStyle(titleColor: UINavigationBar.titleBlackColor)
}
@objc private func userInfoUpdateNotification() {
self.tableView.reloadData()
}
}
extension NRWalletViewController {
private func nr_setupUI() {
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.left.right.bottom.equalToSuperview()
make.top.equalToSuperview().offset(UIScreen.navBarHeight)
}
}
}
//MARK: UITableViewDelegate UITableViewDataSource
extension NRWalletViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! NRWalletCell
cell.item = dataArr[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArr.count
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let view = tableView.dequeueReusableHeaderFooterView(withIdentifier: "header") as! NRWalletHeaderView
view.userInfo = NRLoginManager.manager.userInfo
return view
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = self.dataArr[indexPath.row]
switch item.type {
case .consumptionRecords:
let vc = NRConsumptionRecordsViewController()
self.navigationController?.pushViewController(vc, animated: true)
case .rewardCoins:
let vc = NRRewardCoinsViewController()
self.navigationController?.pushViewController(vc, animated: true)
case .purchaseRecords:
let vc = NROrderRecordsPageViewController()
self.navigationController?.pushViewController(vc, animated: true)
default:
break
}
}
}

View File

@ -23,6 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
Task {
await NRLoginManager.manager.updateUserInfo()
}
NRIapManager.manager.preloadingProducts()
return true
}
@ -43,9 +44,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
@objc private func networkStatusDidChangeNotification() {
guard NRNetworkReachableManager.manager.isReachable == true else {
return
}
Task {
await NRLoginManager.manager.updateUserInfo()
}
NRIapManager.manager.preloadingProducts()
}
}

View File

@ -0,0 +1,41 @@
//
// NRIAPOrderModel.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import SmartCodable
class NRIAPOrderModel: NSObject, SmartCodable {
required override init() { }
var code: Int?
var message: String?
var money: String?
var order_code: String?
var is_backhaul: String?
var discount: NRIAPOrderDiscountModel?
}
class NRIAPOrderDiscountModel: NSObject, SmartCodable {
required override init() { }
var is_discount: Bool?
var discount_code: String?
var sign_data: NRIAPOrderDiscountSign?
}
class NRIAPOrderDiscountSign: NSObject, SmartCodable {
required override init() { }
var keyIdentifier: String?
var nonce: String?
var timestamp: TimeInterval?
var signature: String?
var applicationUsername: String?
}

View File

@ -0,0 +1,19 @@
//
// NRIAPVerifyModel.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import SmartCodable
class NRIAPVerifyModel: NSObject, SmartCodable {
required override init() { }
var code: String?
var is_backhaul: String?
var money: String?
var status: String?
}

View File

@ -0,0 +1,293 @@
//
// NRIapManager.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import StoreKit
class NRIapManager {
typealias CompletionHandler = ((_ finish: Bool) -> Void)
///
static let IAPPrefix = "readerhive"
static let manager = NRIapManager()
///
private var completionHandler: CompletionHandler?
private var shortPlayId: String?
private var videoId: String?
private lazy var iapManager: JXIAPManager = {
let manager = JXIAPManager()
manager.delegate = self
return manager
}()
private var orderCode: String?
private var payId: String?
///
private var payRequest: NRPayDataRequest?
var payDateModel: NRPayDateModel?
///使
///
private var waitRestoreModel: NRWaitRestoreModel? = UserDefaults.nr_object(forKey: kNRWaitRestoreIAPDefaultsKey, as: NRWaitRestoreModel.self)
///
func start(model: NRPayItem, shortPlayId: String? = nil, videoId: String? = nil, hudShowView: UIView? = nil, handler: CompletionHandler? = nil) {
if let _ = self.waitRestoreModel {
NRToast.show(text: "pay_error_2".localized)
handler?(false)
return
}
guard let payId = model.id else {
handler?(false)
return
}
self.shortPlayId = shortPlayId
self.videoId = videoId
self.completionHandler = handler
self.waitRestoreModel = NRWaitRestoreModel()
self.waitRestoreModel?.buyType = model.buy_type
let productId = getProductId(templateId: model.ios_template_id) ?? ""
var isDiscount = false
var identifierDiscount: String? = nil
if model.discount_type == 1, let _ = model.introductionaryOffer {
isDiscount = true
} else if model.discount_type == 2, let discount = model.promotionalOffers?.first {
isDiscount = true
identifierDiscount = discount.identifier
}
NRHud.show(containerView: hudShowView)
NRStoreAPI.requestCreateOrder(payId: payId, shortPlayId: shortPlayId ?? "0", videoId: videoId ?? "0", isDiscount: isDiscount, identifierDiscount: identifierDiscount) { orderModel in
guard let orderModel = orderModel else {
NRHud.dismiss()
self.waitRestoreModel = nil
self.completionHandler?(false)
self.clean()
return
}
self.orderCode = orderModel.order_code
self.payId = payId
self.waitRestoreModel?.payId = payId
self.waitRestoreModel?.orderCode = orderModel.order_code
var discount: SKPaymentDiscount? = nil
if let identifierDiscount = identifierDiscount,
let signData = orderModel.discount?.sign_data,
let keyIdentifier = signData.keyIdentifier,
let nonce = UUID(uuidString: signData.nonce ?? ""),
let signature = signData.signature,
let timestamp = signData.timestamp
{
discount = SKPaymentDiscount(identifier: identifierDiscount,
keyIdentifier: keyIdentifier,
nonce: nonce,
signature: signature,
timestamp: NSNumber(value: timestamp))
}
self.iapManager.start(productId: productId, orderId: self.orderCode ?? "", applicationUsername: orderModel.discount?.sign_data?.applicationUsername, discount: discount)
}
}
func restore(isLoding: Bool = true, shortPlayId: String? = nil, videoId: String? = nil, completer: ((_ isFinish: Bool, _ buyType: NRStoreAPI.BuyType?) -> Void)?) {
let buyType = self.waitRestoreModel?.buyType
guard let waitRestoreModel = self.waitRestoreModel,
let orderCode = waitRestoreModel.orderCode,
let payId = waitRestoreModel.payId,
let receipt = waitRestoreModel.receipt,
let transactionId = waitRestoreModel.transactionId
else {
if isLoding {
NRToast.show(text: "pay_error_3".localized)
}
completer?(false, buyType)
return
}
if isLoding {
NRHud.show()
}
let verifyData = self.getVerifyOrderParameters(orderCode: orderCode, payId: payId, transactionId: transactionId, purchaseToken: receipt)
let statParamenters: [String : Any] = [
"type" : isLoding ? "manual" : "auto",
"pay_data" : verifyData.toJsonString() ?? ""
]
// NRStatAPI.requestEventStat(orderCode: orderCode, shortPlayId: shortPlayId, videoId: videoId, eventKey: .payRestore, errorMsg: "restore", otherParamenters: statParamenters)
NRStoreAPI.requestVerifyOrder(parameters: verifyData) { response in
if isLoding {
NRHud.dismiss()
}
guard let model = response.data else {
completer?(false, buyType)
return
}
self.waitRestoreModel = nil
UserDefaults.nr_setObject(nil, forKey: kNRWaitRestoreIAPDefaultsKey)
if model.status == "success" {
if buyType == .subVip {
NRLoginManager.manager.userInfo?.is_vip = true
}
if isLoding {
NRToast.show(text: "Success".localized)
}
completer?(true, buyType)
if buyType == .subVip {
NotificationCenter.default.post(name: NRIapManager.buyVipFinishNotification, object: nil)
}
} else {
completer?(false, buyType)
}
}
}
func getProductId(templateId: String?) -> String? {
guard let templateId = templateId else { return nil }
return NRIapManager.IAPPrefix + "." + templateId
}
func clean() {
self.orderCode = nil
self.payId = nil
self.shortPlayId = nil
self.videoId = nil
self.completionHandler = nil
}
}
//MARK: JXIAPManagerDelegate
extension NRIapManager: JXIAPManagerDelegate {
func jx_iapPaySuccess(productId: String, receipt: String, transactionIdentifier: String) {
guard let orderCode = self.orderCode, let payId = self.payId else {
self.waitRestoreModel = nil
self.completionHandler?(false)
self.clean()
NRHud.dismiss()
return
}
self.waitRestoreModel?.productId = productId
self.waitRestoreModel?.receipt = receipt
self.waitRestoreModel?.transactionId = transactionIdentifier
UserDefaults.nr_setObject(self.waitRestoreModel, forKey: kNRWaitRestoreIAPDefaultsKey)
#if DEBUG
let verifyData = self.getVerifyOrderParameters(orderCode: orderCode, payId: payId, transactionId: transactionIdentifier, purchaseToken: receipt)
#else
let verifyData = self.getVerifyOrderParameters(orderCode: orderCode, payId: payId, transactionId: transactionIdentifier, purchaseToken: receipt)
#endif
NRStoreAPI.requestVerifyOrder(parameters: verifyData) { response in
NRHud.dismiss()
guard let model = response.data else {
// FAStatAPI.requestEventStat(orderCode: self.orderCode, shortPlayId: self.shortPlayId, videoId: self.videoId, eventKey: .payCallback, errorMsg: verifyData.toJsonString())
self.completionHandler?(false)
self.clean()
return
}
let buyType = self.waitRestoreModel?.buyType
self.waitRestoreModel = nil
UserDefaults.nr_setObject(nil, forKey: kNRWaitRestoreIAPDefaultsKey)
if model.status == "success" {
if buyType == .subVip {
NRLoginManager.manager.userInfo?.is_vip = true
}
NRToast.show(text: "Success".localized)
self.completionHandler?(true)
if buyType == .subVip {
NotificationCenter.default.post(name: NRIapManager.buyVipFinishNotification, object: nil)
}
} else {
NRToast.show(text: "pay_error_4".localized)
// FAStatAPI.requestEventStat(orderCode: self.orderCode, shortPlayId: self.shortPlayId, videoId: self.videoId, eventKey: .payCallback, errorMsg: verifyData.toJsonString())
self.completionHandler?(false)
}
self.clean()
}
}
func jx_iapPayFailed(productId: String, code: JXIAPManagerCode, msg: String?) {
NRHud.dismiss()
if code == .noProduct {
NRToast.show(text: "pay_error_5".localized)
} else if code == .cancelled {
NRToast.show(text: "pay_error_6".localized)
}
// if code == .cancelled {
// FAStatAPI.requestEventStat(orderCode: self.orderCode, shortPlayId: self.shortPlayId, videoId: self.videoId, eventKey: .payCancel, errorMsg: "user cancel")
// } else {
// FAStatAPI.requestEventStat(orderCode: self.orderCode, shortPlayId: self.shortPlayId, videoId: self.videoId, eventKey: .payError, errorMsg: msg)
// }
self.completionHandler?(false)
self.waitRestoreModel = nil
self.clean()
}
}
extension NRIapManager {
func getVerifyOrderParameters(orderCode: String, payId: String, transactionId: String, purchaseToken: String) -> [String : Any] {
let parameters: [String : Any] = [
"order_code" : orderCode,
"pay_setting_id" : payId,
"pkg_name" : kNRAPPBundleIdentifier,
"transaction_id": transactionId,
"purchases_token" : purchaseToken
]
return parameters
}
///
func preloadingProducts() {
JXIAPManager.manager.fetchReceipt { _ in
self.payRequest = NRPayDataRequest()
self.payRequest?.requestProducts(isLoding: false, isToast: false) { model in
}
}
}
}
extension NRIapManager {
///
@objc static let buyVipFinishNotification = Notification.Name(rawValue: "NRIapManager.buyVipFinishNotification")
}

View File

@ -0,0 +1,179 @@
//
// NRPayDataRequest.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import StoreKit
class NRPayDataRequest: NSObject {
private var oldTemplateModel: NRPayDateModel?
private(set) var newTemplateModel: NRPayDateModel?
// private var payAlertModel: NRPayAlertModel?
private var completerBlock: ((_ model: NRPayDateModel?) -> Void)?
// private var payAlertBlock: ((_ model: FAPayAlertModel?) -> Void)?
private var isLoding = false
private var isToast = false
func requestProducts(isLoding: Bool = false, isToast: Bool = true, completer: @escaping ((_ model: NRPayDateModel?) -> Void)) {
// self.payAlertBlock = nil
self.completerBlock = completer
self.isLoding = isLoding
self.isToast = isToast
if isLoding { NRHud.show() }
NRStoreAPI.requestPayTemplate(isToast: isToast) { [weak self] model in
guard let self = self else { return }
guard let model = model else {
if isLoding { NRHud.dismiss() }
self.completerBlock?(nil)
return
}
self.oldTemplateModel = model
var productIdArr: [String] = []
model.list_sub_vip?.forEach { item in
productIdArr.append(NRIapManager.manager.getProductId(templateId: item.ios_template_id) ?? "")
}
model.list_coins?.forEach { item in
productIdArr.append(NRIapManager.manager.getProductId(templateId: item.ios_template_id) ?? "")
}
let set = Set(productIdArr)
let productsRequest = SKProductsRequest(productIdentifiers: set)
productsRequest.delegate = self
productsRequest.start()
}
}
// func requestCoinAlertData(completer: @escaping ((_ model: FAPayAlertModel?) -> Void)) {
// self.completerBlock = nil
// self.payAlertBlock = completer
// BRStoreAPI.requestCoinAlertInfo { [weak self] model in
// guard let self = self else { return }
//
// guard let model = model else {
// self.payAlertBlock?(nil)
// return
// }
// self.payAlertModel = model
//
// let productId = BRIAP.manager.getProductId(templateId: model.info?.ios_template_id) ?? ""
//
// let set = Set([productId])
// let productsRequest = SKProductsRequest(productIdentifiers: set)
// productsRequest.delegate = self
// productsRequest.start()
// }
// }
///
// func requestVipRetainPayInfo(completer: ((_ model: FAPayAlertModel?) -> Void)?) {
// self.completerBlock = nil
// self.payAlertBlock = completer
//
// FAStoreAPI.requestVipRetainPayInfo { [weak self] model in
// guard let self = self else { return }
// guard let model = model else {
// self.payAlertBlock?(nil)
// return
// }
// self.payAlertModel = model
//
// let productId = FAIapManager.manager.getProductId(templateId: model.info?.ios_template_id) ?? ""
//
// let set = Set([productId])
// let productsRequest = SKProductsRequest(productIdentifiers: set)
// productsRequest.delegate = self
// productsRequest.start()
// }
// }
}
//MARK: SKProductsRequestDelegate
extension NRPayDataRequest: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
if isLoding {
NRHud.dismiss()
}
let products = response.products
if let block = self.completerBlock {
guard let templateModel = self.oldTemplateModel else { return }
var newCoinList: [NRPayItem] = []
var newVipList: [NRPayItem] = []
templateModel.list_coins?.forEach { item in
let productId = NRIapManager.manager.getProductId(templateId: item.ios_template_id) ?? ""
for product in products {
if productId == product.productIdentifier {
item.price = product.price.stringValue
item.currency = product.priceLocale.currencySymbol
item.product = product
newCoinList.append(item)
break
}
}
}
templateModel.list_sub_vip?.forEach { item in
let productId = NRIapManager.manager.getProductId(templateId: item.ios_template_id) ?? ""
for product in products {
if productId == product.productIdentifier {
item.price = product.price.stringValue
item.currency = product.priceLocale.currencySymbol
item.product = product
newVipList.append(item)
break
}
}
}
templateModel.list_coins = newCoinList
templateModel.list_sub_vip = newVipList
self.newTemplateModel = templateModel
NRIapManager.manager.payDateModel = templateModel
DispatchQueue.main.async {
block(templateModel)
}
}
// else if let block = self.payAlertBlock {
// guard let coinalertModel = self.payAlertModel else { return }
// let productId = FAIapManager.manager.getProductId(templateId: coinalertModel.info?.ios_template_id) ?? ""
//
// for product in products {
// if productId == product.productIdentifier {
// coinalertModel.info?.price = product.price.stringValue
// coinalertModel.info?.currency = product.priceLocale.currencySymbol
// coinalertModel.info?.product = product
// break
// }
// }
//
// DispatchQueue.main.async {
// block(coinalertModel)
// }
// }
}
}

View File

@ -0,0 +1,48 @@
//
// NRWaitRestoreModel.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
class NRWaitRestoreModel: NSObject, NSSecureCoding{
var orderCode: String?
var buyType: NRStoreAPI.BuyType?
var payId: String?
var productId: String?
var transactionId: String?
var receipt: String?
required override init() { }
static var supportsSecureCoding: Bool {
get {
return true
}
}
func encode(with coder: NSCoder) {
coder.encode(orderCode, forKey: "orderCode")
coder.encode(payId, forKey: "payId")
coder.encode(productId, forKey: "productId")
coder.encode(receipt, forKey: "receipt")
coder.encode(buyType?.rawValue, forKey: "buyType")
coder.encode(transactionId, forKey: "transactionId")
}
required init?(coder: NSCoder) {
super.init()
orderCode = coder.decodeObject(of: NSString.self, forKey: "orderCode") as? String
payId = coder.decodeObject(of: NSString.self, forKey: "payId") as? String
productId = coder.decodeObject(of: NSString.self, forKey: "productId") as? String
receipt = coder.decodeObject(of: NSString.self, forKey: "receipt") as? String
transactionId = coder.decodeObject(of: NSString.self, forKey: "transactionId") as? String
if let type = coder.decodeObject(of: NSString.self, forKey: "buyType") as? String {
buyType = NRStoreAPI.BuyType(rawValue: type)
}
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "arrow@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "arrow@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "My Wallet@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "My Wallet@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Gift@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Gift@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Banner-DailyReward@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Banner-DailyReward@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Vector@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Vector@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "time icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "time icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -83,8 +83,36 @@
"Top Up" = "Top Up";
"Language" = "Language";
"Loading" = "Loading";
"Store" = "Store";
"Coins Purchase" = "Coins Purchase";
"VIP Membership" = "VIP Membership";
"Auto renew,cancel anytime" = "Auto renew,cancel anytime";
"Feedback" = "Feedback";
"Feedback History" = "Feedback History";
"Feedback Details" = "Feedback Details";
"Account Deletion" = "Account Deletion";
"me_coins_pack_title" = "Daily reward ready!";
"me_coins_pack_subtitle" = "Claim your rewards now.";
"Weekly VIP" = "Weekly VIP";
"My Wallet" = "My Wallet";
"Consumption Records" = "Consumption Records";
"Purchase Records" = "Purchase Records";
"Reward Coins" = "Reward Coins";
"Purchase Single Episode" = "Purchase Single Episode";
"Expired" = "Expired";
"Expires in ## days" = "Expires in ## days";
"Order Records" = "Order Records";
"Coin Record" = "Coin Record";
"VIP Record" = "VIP Record";
"pay_error_1" = "You are already a member!";
"pay_error_2" = "You have unfinished in-app purchases, please restore them first.";
"pay_error_3" = "There are no recoverable in-app purchases.";
"pay_error_4" = "Purchase Failed";
"pay_error_5" = "Invalid in-app purchase";
"pay_error_6" = "Payment has been cancelled";
"network_error_1" = "Your account is already logged in on another device~";
"network_error_2" = "The service is abnormal. Check the network.";

View File

@ -0,0 +1,223 @@
//
// JXIAPManager.swift
// ReaderHive
//
// Created by 鸿 on 2025/12/10.
//
import UIKit
import StoreKit
@objc protocol JXIAPManagerDelegate {
///
@objc optional func jx_iapPaySuccess(productId: String, receipt: String, transactionIdentifier: String)
///
@objc optional func jx_iapPayFailed(productId: String, code: JXIAPManagerCode, msg: String?)
///
@objc optional func iapPayRestore(productIds: [String], transactionIds: [String])
// ///
// @objc optional func iapPayShowHud()
// ///
// @objc optional func iapSysWrong()
// ///
// @objc optional func verifySuccess()
// ///
// @objc optional func verifyFailed()
}
@objc enum JXIAPManagerCode: Int {
///
case unknown
///
case cancelled
///
case noProduct
}
class JXIAPManager: NSObject {
static let manager: JXIAPManager = JXIAPManager()
weak var delegate: JXIAPManagerDelegate?
private var payment: SKPayment?
private var product: SKProduct?
private var productId: String?
private var discount: SKPaymentDiscount?
private var orderId: String?
private var applicationUsername: String?
private var receiptCompletion: ((URL?) -> Void)?
override init() {
super.init()
SKPaymentQueue.default().add(self)
}
func start(productId: String, orderId: String, applicationUsername: String?, discount: SKPaymentDiscount? = nil) {
self.product = nil
self.productId = productId
self.orderId = orderId
self.discount = discount
self.applicationUsername = applicationUsername
let set = Set([productId])
let productsRequest = SKProductsRequest(productIdentifiers: set)
productsRequest.delegate = self
productsRequest.start()
}
///
private func buyProduct() {
guard let product = self.product else { return }
//
let payment = SKMutablePayment(product: product)
payment.applicationUsername = self.applicationUsername
if let discount = self.discount {
payment.paymentDiscount = discount
self.discount = nil
}
self.payment = payment
//
SKPaymentQueue.default().add(payment)
}
}
//MARK: -------------- SKProductsRequestDelegate --------------
extension JXIAPManager: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
guard let product = response.products.first else {
DispatchQueue.main.async {
if let productId = self.productId {
self.productId = nil
self.delegate?.jx_iapPayFailed?(productId: productId, code: .noProduct, msg: nil)
}
}
return
}
self.product = product
self.buyProduct()
}
}
//MARK: -------------- SKPaymentTransactionObserver --------------
extension JXIAPManager: SKPaymentTransactionObserver {
///
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased:
DispatchQueue.main.async {
self.completeTransaction(transaction: transaction)
}
SKPaymentQueue.default().finishTransaction(transaction)
case .failed:
DispatchQueue.main.async {
self.failedTransaction(transaction: transaction)
}
SKPaymentQueue.default().finishTransaction(transaction)
// case .restored:
// self.restoreTransaction(transaction: transaction)
case .purchasing:
break
default:
SKPaymentQueue.default().finishTransaction(transaction)
break
}
}
}
// func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
// return true
// }
///
func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
}
}
extension JXIAPManager {
private func completeTransaction(transaction: SKPaymentTransaction) {
guard let encodeStr = getAppStoreReceipt() else { return }
guard let transactionIdentifier = transaction.transactionIdentifier else { return }
guard let productId = self.productId, productId == transaction.payment.productIdentifier else { return }
self.productId = nil
self.delegate?.jx_iapPaySuccess?(productId: productId, receipt: encodeStr, transactionIdentifier: transactionIdentifier)
}
private func failedTransaction(transaction: SKPaymentTransaction) {
let error = transaction.error as? SKError
guard let productId = self.productId else { return }
self.productId = nil
switch error?.code {
case SKError.paymentCancelled:
self.delegate?.jx_iapPayFailed?(productId: productId, code: .cancelled, msg: error?.localizedDescription)
default:
self.delegate?.jx_iapPayFailed?(productId: productId, code: .unknown, msg: error?.localizedDescription)
}
}
}
extension JXIAPManager: SKRequestDelegate {
func getAppStoreReceipt() -> String? {
guard let receiptURL = Bundle.main.appStoreReceiptURL else { return nil }
let receiptData = NSData(contentsOf: receiptURL)
return receiptData?.base64EncodedString(options: .endLineWithLineFeed)
}
///
func fetchReceipt(completion: @escaping (URL?) -> Void) {
let receiptURL = Bundle.main.appStoreReceiptURL
if let url = receiptURL, FileManager.default.fileExists(atPath: url.path) {
completion(url)
return
}
let request = SKReceiptRefreshRequest()
request.delegate = self
request.start()
// delegate
self.receiptCompletion = completion
}
func requestDidFinish(_ request: SKRequest) {
let receiptURL = Bundle.main.appStoreReceiptURL
if let url = receiptURL, FileManager.default.fileExists(atPath: url.path) {
receiptCompletion?(url)
} else {
receiptCompletion?(nil)
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
print("Receipt request failed: \(error)")
receiptCompletion?(nil)
}
}