using System.Net.Http.Json; using System.Text.Json; namespace Hokm.Server.Payments; /// /// Config for store in-app billing verification. Fill these from the Cafe Bazaar /// (pardakht) and Myket developer panels. Bound from the "Iab" config section / /// Iab__* env vars. /// public sealed class IabOptions { /// Android package name registered in the store panels. public string PackageName { get; set; } = "com.bargevasat.app"; // ── Cafe Bazaar (pardakht dev API, OAuth refresh-token flow) ── public string BazaarClientId { get; set; } = ""; public string BazaarClientSecret { get; set; } = ""; public string BazaarRefreshToken { get; set; } = ""; /// /// Cafe Bazaar in-app billing RSA public key (panel: «دریافت کلید RSA برای /// قراردادن در برنامه»). Used to verify a purchase payload's signature locally /// (Poolakey in-app library flow). NOT used by the current deep-link flow, /// which verifies server-to-server via the pardakht API above — kept here so /// the key has a home if/when the native Poolakey plugin is added. /// public string BazaarRsaPublicKey { get; set; } = ""; // ── Myket (developer validation API) ── public string MyketAccessToken { get; set; } = ""; /// /// DEV ONLY. When true, purchases are credited WITHOUT remote verification /// (use for local testing before you have store credentials). NEVER enable in /// production — it lets a forged token mint coins. /// public bool AllowUnverified { get; set; } = false; } /// /// Verifies a store purchase token (Cafe Bazaar / Myket) server-to-server before /// coins are credited. Endpoints are config-driven; confirm the exact URLs against /// your store panel — the request/response shapes mirror Google Play's IAB API. /// public sealed class IabService { private static readonly HttpClient Http = new(); private readonly IabOptions _opts; private readonly ILogger _log; public IabService(IabOptions opts, ILogger log) { _opts = opts; _log = log; } public async Task Verify(string store, string productId, string token) { if (string.IsNullOrWhiteSpace(token)) return _opts.AllowUnverified; store = (store ?? "").Trim().ToLowerInvariant(); try { return store switch { "bazaar" or "cafebazaar" => await VerifyBazaar(productId, token), "myket" => await VerifyMyket(productId, token), _ => _opts.AllowUnverified, }; } catch (Exception ex) { _log.LogWarning(ex, "IAB verify failed for store {Store} product {Product}", store, productId); return _opts.AllowUnverified; } } /// /// Cafe Bazaar: exchange the refresh token for an access token, then validate /// the in-app purchase. See https://pardakht.cafebazaar.ir/devapi/v2/. /// private async Task VerifyBazaar(string productId, string token) { if (string.IsNullOrWhiteSpace(_opts.BazaarRefreshToken)) return _opts.AllowUnverified; // 1) refresh_token → access_token var form = new FormUrlEncodedContent(new Dictionary { ["grant_type"] = "refresh_token", ["client_id"] = _opts.BazaarClientId, ["client_secret"] = _opts.BazaarClientSecret, ["refresh_token"] = _opts.BazaarRefreshToken, }); var tokenResp = await Http.PostAsync("https://pardakht.cafebazaar.ir/devapi/v2/auth/token/", form); if (!tokenResp.IsSuccessStatusCode) return false; using var tokenDoc = JsonDocument.Parse(await tokenResp.Content.ReadAsStringAsync()); if (!tokenDoc.RootElement.TryGetProperty("access_token", out var at)) return false; var access = at.GetString(); // 2) validate the purchase var url = $"https://pardakht.cafebazaar.ir/devapi/v2/api/validate/{_opts.PackageName}/inapp/{Uri.EscapeDataString(productId)}/purchases/{Uri.EscapeDataString(token)}/?access_token={access}"; var vResp = await Http.GetAsync(url); if (!vResp.IsSuccessStatusCode) return false; using var vDoc = JsonDocument.Parse(await vResp.Content.ReadAsStringAsync()); // purchaseState: 0 = purchased (1 = refunded/cancelled). Absent ⇒ a 200 body // is itself proof of a valid purchase. if (vDoc.RootElement.TryGetProperty("purchaseState", out var ps) && ps.ValueKind == JsonValueKind.Number) return ps.GetInt32() == 0; return true; } /// /// Myket: validate via the developer API. POST the purchase token in the body /// (`{ "tokenId": ... }`) to the partners/verify endpoint with an X-Access-Token /// header. The access token comes from the Myket developer panel → in-app /// products. See https://myket.ir/kb/pages/server-to-server-payment-validation-api/ /// private async Task VerifyMyket(string productId, string token) { if (string.IsNullOrWhiteSpace(_opts.MyketAccessToken)) return _opts.AllowUnverified; var url = $"https://developer.myket.ir/api/partners/applications/{_opts.PackageName}/purchases/products/{Uri.EscapeDataString(productId)}/verify"; using var req = new HttpRequestMessage(HttpMethod.Post, url); req.Headers.Add("X-Access-Token", _opts.MyketAccessToken); req.Content = new StringContent( JsonSerializer.Serialize(new { tokenId = token }), System.Text.Encoding.UTF8, "application/json"); var resp = await Http.SendAsync(req); if (!resp.IsSuccessStatusCode) return false; using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()); // purchaseState: 0 = successful purchase, 1 = failed. if (doc.RootElement.TryGetProperty("purchaseState", out var ps) && ps.ValueKind == JsonValueKind.Number) return ps.GetInt32() == 0; return true; } }