feat: enhance StoreKit2Helper to handle entitlements and improve restore service from App Store functionality

This commit is contained in:
spectrum
2026-03-24 17:52:46 +02:00
parent 6de556e730
commit 4e4f8a5ec5
2 changed files with 109 additions and 50 deletions

View File

@@ -7,24 +7,47 @@ public class StoreKit2Helper: NSObject {
public static let shared = StoreKit2Helper()
private struct EntitlementInfo {
let transactionId: UInt64
let originalTransactionId: UInt64
let productId: String
let purchaseDate: Date
var dictionary: NSDictionary {
[
"transactionId": String(transactionId),
"originalTransactionId": String(originalTransactionId),
"productId": productId
]
}
}
public func fetchCurrentEntitlements(completion: @escaping (Bool, [NSDictionary]?, NSError?) -> Void) {
Task {
var entitlements: [NSDictionary] = []
Task { @MainActor in
do {
try await AppStore.sync()
var entitlements: [EntitlementInfo] = []
for await result in Transaction.currentEntitlements {
switch result {
case .verified(let transaction):
let info: NSDictionary = [
"transactionId": String(transaction.id),
"originalTransactionId": String(transaction.originalID),
"productId": transaction.productID
]
entitlements.append(info)
entitlements.append(EntitlementInfo(transactionId: transaction.id,
originalTransactionId: transaction.originalID,
productId: transaction.productID,
purchaseDate: transaction.purchaseDate))
case .unverified(_, let error):
print("[IAP][StoreKit2] Unverified transaction skipped: \(error.localizedDescription)")
}
}
DispatchQueue.main.async { completion(true, entitlements, nil) }
let sortedEntitlements = entitlements.sorted { lhs, rhs in
if lhs.purchaseDate != rhs.purchaseDate {
return lhs.purchaseDate > rhs.purchaseDate
}
return lhs.transactionId > rhs.transactionId
}.map { $0.dictionary }
completion(true, sortedEntitlements, nil)
} catch {
completion(false, nil, error as NSError)
}
}
}

View File

@@ -576,52 +576,88 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
return false;
}
const QVariantMap &transaction = restoredTransactions.first();
const QString originalTransactionId = transaction.value(QStringLiteral("originalTransactionId")).toString();
const QString transactionId = transaction.value(QStringLiteral("transactionId")).toString();
const QString productId = transaction.value(QStringLiteral("productId")).toString();
const bool isTestPurchase = IosController::Instance()->isTestFlight();
const QString serviceType = m_apiServicesModel->getSelectedServiceType();
const QString serviceProtocol = m_apiServicesModel->getSelectedServiceProtocol();
const QString countryCode = m_apiServicesModel->getCountryCode();
const QString appLanguage = m_settings->getAppLanguage().name().split("_").first();
const QString installationUuid = m_settings->getInstallationUuid(true);
bool hasInstalledConfig = false;
bool duplicateConfigAlreadyPresent = false;
int duplicateServerIndex = -1;
QSet<QString> processedOriginalTransactionIds;
for (const QVariantMap &transaction : restoredTransactions) {
const QString originalTransactionId = transaction.value(QStringLiteral("originalTransactionId")).toString();
const QString transactionId = transaction.value(QStringLiteral("transactionId")).toString();
const QString productId = transaction.value(QStringLiteral("productId")).toString();
if (originalTransactionId.isEmpty()) {
qWarning().noquote() << "[IAP] Skipping restored transaction without originalTransactionId" << transactionId;
continue;
}
if (processedOriginalTransactionIds.contains(originalTransactionId)) {
qInfo().noquote() << "[IAP] Skipping duplicate restored transaction" << originalTransactionId;
continue;
}
processedOriginalTransactionIds.insert(originalTransactionId);
qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId
<< "originalTransactionId =" << originalTransactionId << "productId =" << productId;
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
appLanguage,
installationUuid,
countryCode,
"",
serviceType,
serviceProtocol,
QJsonObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::transactionId] = originalTransactionId;
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
qWarning().noquote() << "[IAP] Failed to restore transaction" << originalTransactionId
<< "errorCode =" << static_cast<int>(errorCode);
continue;
}
int currentDuplicateServerIndex = -1;
errorCode = importServiceFromBilling(responseBody, isTestPurchase, currentDuplicateServerIndex);
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
duplicateConfigAlreadyPresent = true;
if (duplicateServerIndex < 0) {
duplicateServerIndex = currentDuplicateServerIndex;
}
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists" << originalTransactionId;
continue;
}
if (errorCode != ErrorCode::NoError) {
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId
<< "errorCode =" << static_cast<int>(errorCode);
continue;
}
hasInstalledConfig = true;
}
if (!hasInstalledConfig) {
if (duplicateConfigAlreadyPresent) {
emit installServerFromApiFinished(tr("This subscription is already in the app."), duplicateServerIndex);
return true;
}
if (originalTransactionId.isEmpty()) {
qWarning().noquote() << "[IAP] Active entitlement has no originalTransactionId";
emit errorOccurred(ErrorCode::ApiPurchaseError);
return false;
}
qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId
<< "originalTransactionId =" << originalTransactionId << "productId =" << productId;
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
m_apiServicesModel->getCountryCode(),
"",
m_apiServicesModel->getSelectedServiceType(),
m_apiServicesModel->getSelectedServiceProtocol(),
QJsonObject() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::transactionId] = originalTransactionId;
auto isTestPurchase = IosController::Instance()->isTestFlight();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
qWarning().noquote() << "[IAP] Failed to restore transaction" << originalTransactionId
<< "errorCode =" << static_cast<int>(errorCode);
emit errorOccurred(errorCode);
return false;
}
int duplicateServerIndex = -1;
errorCode = importServiceFromBilling(responseBody, isTestPurchase, duplicateServerIndex);
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
emit installServerFromApiFinished(tr("This subscription is already in the app."), duplicateServerIndex);
return true;
}
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return false;
}
emit installServerFromApiFinished(tr("Subscription restored successfully."));
#endif
return true;