diff --git a/.benchmark/REPORT.md b/.benchmark/REPORT.md index 18689b97e..61d5f3f0f 100644 --- a/.benchmark/REPORT.md +++ b/.benchmark/REPORT.md @@ -4,9 +4,9 @@ Time from `preloadAllPaywalls()` until every paywall reaches `PaywallLoadingStat | Tier | Paywalls | Mean | Baseline | Δ | Limit | Cold mean | Warm mean | Median | StdDev | CV | Samples | Status | |------|----------|------|----------|---|-------|-----------|-----------|--------|--------|----|---------|--------| -| LOW | 10 | 6.29s | 6.34s | -0.7% | +30% | 8.05s | — | 6.29s | 3.68s | 45.7% | 10 (10×1) | 🟢 improved | -| MID | 10 | 6.19s | 8.78s | -29.5% | +15% | 7.78s | — | 6.19s | 3.26s | 41.9% | 10 (10×1) | 🟢 improved | -| HIGH | 10 | 11.14s | 6.72s | +65.9% | +20% | 10.53s | — | 11.14s | 3.60s | 34.2% | 10 (10×1) | ❌ regression | +| LOW | 10 | 7.20s | 6.34s | +13.6% | +30% | 7.86s | — | 7.20s | 2.49s | 31.7% | 10 (10×1) | ✅ OK | +| MID | 10 | 6.03s | 8.78s | -31.3% | +15% | 6.92s | — | 6.03s | 2.76s | 40.0% | 10 (10×1) | 🟢 improved | +| HIGH | 10 | 6.36s | 6.72s | -5.3% | +20% | 6.73s | — | 6.36s | 2.21s | 32.8% | 10 (10×1) | 🟢 improved | ### Devices diff --git a/.benchmark/results/preload-benchmark-high.json b/.benchmark/results/preload-benchmark-high.json index 0c56e0e2d..19e2a3512 100644 --- a/.benchmark/results/preload-benchmark-high.json +++ b/.benchmark/results/preload-benchmark-high.json @@ -11,226 +11,226 @@ "runCount": 10, "samples": [ { - "allReadyMs": 15509, + "allReadyMs": 12422, "cold": true, - "configureMs": 3708, + "configureMs": 2614, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 10547, - "data-v3-0ed9-2024-04-04": 11875, - "free-trial-v3-d8d7-2024-04-04": 15509, - "inimai---open-app-copy-c50c-2024-11-28": 9576, - "localized-paywall-e901-2024-04-04": 11364, - "price-tester-1b8e-2024-04-04": 12284, - "products-v3-60c5-2024-04-04": 12130, - "superwall-template-9f9f-2024-05-21": 9474, - "url-paywall-v3-3810-2024-04-04": 11109, - "video-v3-72b4-2024-04-04": 9985 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 12422, + "data-v3-0ed9-2024-04-04": 11606, + "free-trial-v3-d8d7-2024-04-04": 10149, + "inimai---open-app-copy-c50c-2024-11-28": 7040, + "localized-paywall-e901-2024-04-04": 10358, + "price-tester-1b8e-2024-04-04": 10875, + "products-v3-60c5-2024-04-04": 6888, + "superwall-template-9f9f-2024-05-21": 7706, + "url-paywall-v3-3810-2024-04-04": 8504, + "video-v3-72b4-2024-04-04": 10720 }, - "preloadCompleteEventMs": 5184, + "preloadCompleteEventMs": 1928, "run": 1 }, { - "allReadyMs": 10069, + "allReadyMs": 6417, "cold": true, - "configureMs": 2039, + "configureMs": 1581, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 7557, - "data-v3-0ed9-2024-04-04": 9914, - "free-trial-v3-d8d7-2024-04-04": 8327, - "inimai---open-app-copy-c50c-2024-11-28": 6251, - "localized-paywall-e901-2024-04-04": 7865, - "price-tester-1b8e-2024-04-04": 8429, - "products-v3-60c5-2024-04-04": 9304, - "superwall-template-9f9f-2024-05-21": 6662, - "url-paywall-v3-3810-2024-04-04": 9092, - "video-v3-72b4-2024-04-04": 10069 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 6011, + "data-v3-0ed9-2024-04-04": 6417, + "free-trial-v3-d8d7-2024-04-04": 5198, + "inimai---open-app-copy-c50c-2024-11-28": 4163, + "localized-paywall-e901-2024-04-04": 5300, + "price-tester-1b8e-2024-04-04": 4009, + "products-v3-60c5-2024-04-04": 5656, + "superwall-template-9f9f-2024-05-21": 4942, + "url-paywall-v3-3810-2024-04-04": 4576, + "video-v3-72b4-2024-04-04": 5503 }, - "preloadCompleteEventMs": 1441, + "preloadCompleteEventMs": 1011, "run": 2 }, { - "allReadyMs": 12216, + "allReadyMs": 7917, "cold": true, - "configureMs": 3075, + "configureMs": 1980, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 12216, - "data-v3-0ed9-2024-04-04": 10500, - "free-trial-v3-d8d7-2024-04-04": 6911, - "inimai---open-app-copy-c50c-2024-11-28": 7221, - "localized-paywall-e901-2024-04-04": 8575, - "price-tester-1b8e-2024-04-04": 6131, - "products-v3-60c5-2024-04-04": 11491, - "superwall-template-9f9f-2024-05-21": 8003, - "url-paywall-v3-3810-2024-04-04": 9304, - "video-v3-72b4-2024-04-04": 10866 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 7308, + "data-v3-0ed9-2024-04-04": 6902, + "free-trial-v3-d8d7-2024-04-04": 6291, + "inimai---open-app-copy-c50c-2024-11-28": 4787, + "localized-paywall-e901-2024-04-04": 7917, + "price-tester-1b8e-2024-04-04": 4635, + "products-v3-60c5-2024-04-04": 5723, + "superwall-template-9f9f-2024-05-21": 5211, + "url-paywall-v3-3810-2024-04-04": 6495, + "video-v3-72b4-2024-04-04": 7003 }, - "preloadCompleteEventMs": 1835, + "preloadCompleteEventMs": 1539, "run": 3 }, { - "allReadyMs": 14229, + "allReadyMs": 6911, "cold": true, - "configureMs": 1909, + "configureMs": 2074, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 10861, - "data-v3-0ed9-2024-04-04": 14229, - "free-trial-v3-d8d7-2024-04-04": 11915, - "inimai---open-app-copy-c50c-2024-11-28": 12433, - "localized-paywall-e901-2024-04-04": 11394, - "price-tester-1b8e-2024-04-04": 9931, - "products-v3-60c5-2024-04-04": 9263, - "superwall-template-9f9f-2024-05-21": 8115, - "url-paywall-v3-3810-2024-04-04": 6801, - "video-v3-72b4-2024-04-04": 10295 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5795, + "data-v3-0ed9-2024-04-04": 6911, + "free-trial-v3-d8d7-2024-04-04": 4931, + "inimai---open-app-copy-c50c-2024-11-28": 4370, + "localized-paywall-e901-2024-04-04": 4728, + "price-tester-1b8e-2024-04-04": 4982, + "products-v3-60c5-2024-04-04": 5236, + "superwall-template-9f9f-2024-05-21": 4269, + "url-paywall-v3-3810-2024-04-04": 4576, + "video-v3-72b4-2024-04-04": 5134 }, - "preloadCompleteEventMs": 1105, + "preloadCompleteEventMs": 1091, "run": 4 }, { - "allReadyMs": 8974, + "allReadyMs": 6631, "cold": true, - "configureMs": 2418, + "configureMs": 1404, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 8414, - "data-v3-0ed9-2024-04-04": 8109, - "free-trial-v3-d8d7-2024-04-04": 6059, - "inimai---open-app-copy-c50c-2024-11-28": 8821, - "localized-paywall-e901-2024-04-04": 5696, - "price-tester-1b8e-2024-04-04": 7141, - "products-v3-60c5-2024-04-04": 7448, - "superwall-template-9f9f-2024-05-21": 6785, - "url-paywall-v3-3810-2024-04-04": 8974, - "video-v3-72b4-2024-04-04": 7702 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 6175, + "data-v3-0ed9-2024-04-04": 6631, + "free-trial-v3-d8d7-2024-04-04": 5364, + "inimai---open-app-copy-c50c-2024-11-28": 4533, + "localized-paywall-e901-2024-04-04": 5414, + "price-tester-1b8e-2024-04-04": 5668, + "products-v3-60c5-2024-04-04": 5820, + "superwall-template-9f9f-2024-05-21": 5059, + "url-paywall-v3-3810-2024-04-04": 5617, + "video-v3-72b4-2024-04-04": 5922 }, - "preloadCompleteEventMs": 1570, + "preloadCompleteEventMs": 601, "run": 5 }, { - "allReadyMs": 13263, + "allReadyMs": 5498, "cold": true, - "configureMs": 2638, + "configureMs": 1391, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 9505, - "data-v3-0ed9-2024-04-04": 8994, - "free-trial-v3-d8d7-2024-04-04": 13263, - "inimai---open-app-copy-c50c-2024-11-28": 7807, - "localized-paywall-e901-2024-04-04": 8268, - "price-tester-1b8e-2024-04-04": 11088, - "products-v3-60c5-2024-04-04": 9861, - "superwall-template-9f9f-2024-05-21": 10782, - "url-paywall-v3-3810-2024-04-04": 10065, - "video-v3-72b4-2024-04-04": 7604 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5092, + "data-v3-0ed9-2024-04-04": 4585, + "free-trial-v3-d8d7-2024-04-04": 5396, + "inimai---open-app-copy-c50c-2024-11-28": 3773, + "localized-paywall-e901-2024-04-04": 5498, + "price-tester-1b8e-2024-04-04": 4281, + "products-v3-60c5-2024-04-04": 5244, + "superwall-template-9f9f-2024-05-21": 3672, + "url-paywall-v3-3810-2024-04-04": 4026, + "video-v3-72b4-2024-04-04": 3925 }, - "preloadCompleteEventMs": 2635, + "preloadCompleteEventMs": 873, "run": 6 }, { - "allReadyMs": 12855, + "allReadyMs": 5174, "cold": true, - "configureMs": 2565, + "configureMs": 1793, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 11426, - "data-v3-0ed9-2024-04-04": 12855, - "free-trial-v3-d8d7-2024-04-04": 10712, - "inimai---open-app-copy-c50c-2024-11-28": 8944, - "localized-paywall-e901-2024-04-04": 9926, - "price-tester-1b8e-2024-04-04": 11884, - "products-v3-60c5-2024-04-04": 9362, - "superwall-template-9f9f-2024-05-21": 8419, - "url-paywall-v3-3810-2024-04-04": 10916, - "video-v3-72b4-2024-04-04": 11527 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 4311, + "data-v3-0ed9-2024-04-04": 4565, + "free-trial-v3-d8d7-2024-04-04": 3803, + "inimai---open-app-copy-c50c-2024-11-28": 3549, + "localized-paywall-e901-2024-04-04": 5174, + "price-tester-1b8e-2024-04-04": 3447, + "products-v3-60c5-2024-04-04": 4717, + "superwall-template-9f9f-2024-05-21": 5022, + "url-paywall-v3-3810-2024-04-04": 4006, + "video-v3-72b4-2024-04-04": 3904 }, - "preloadCompleteEventMs": 2529, + "preloadCompleteEventMs": 883, "run": 7 }, { - "allReadyMs": 5826, + "allReadyMs": 5108, "cold": true, - "configureMs": 1347, + "configureMs": 1656, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5213, - "data-v3-0ed9-2024-04-04": 5826, - "free-trial-v3-d8d7-2024-04-04": 4028, - "inimai---open-app-copy-c50c-2024-11-28": 3875, - "localized-paywall-e901-2024-04-04": 4435, - "price-tester-1b8e-2024-04-04": 4959, - "products-v3-60c5-2024-04-04": 3468, - "superwall-template-9f9f-2024-05-21": 3723, - "url-paywall-v3-3810-2024-04-04": 4334, - "video-v3-72b4-2024-04-04": 4180 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 4552, + "data-v3-0ed9-2024-04-04": 4805, + "free-trial-v3-d8d7-2024-04-04": 4197, + "inimai---open-app-copy-c50c-2024-11-28": 3324, + "localized-paywall-e901-2024-04-04": 3631, + "price-tester-1b8e-2024-04-04": 4247, + "products-v3-60c5-2024-04-04": 3222, + "superwall-template-9f9f-2024-05-21": 5108, + "url-paywall-v3-3810-2024-04-04": 3476, + "video-v3-72b4-2024-04-04": 4044 }, - "preloadCompleteEventMs": 965, + "preloadCompleteEventMs": 589, "run": 8 }, { - "allReadyMs": 6134, + "allReadyMs": 6305, "cold": true, - "configureMs": 1891, + "configureMs": 1512, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 6134, - "data-v3-0ed9-2024-04-04": 5626, - "free-trial-v3-d8d7-2024-04-04": 5066, - "inimai---open-app-copy-c50c-2024-11-28": 4200, - "localized-paywall-e901-2024-04-04": 5117, - "price-tester-1b8e-2024-04-04": 4353, - "products-v3-60c5-2024-04-04": 4036, - "superwall-template-9f9f-2024-05-21": 4710, - "url-paywall-v3-3810-2024-04-04": 5779, - "video-v3-72b4-2024-04-04": 5320 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 4071, + "data-v3-0ed9-2024-04-04": 6305, + "free-trial-v3-d8d7-2024-04-04": 4782, + "inimai---open-app-copy-c50c-2024-11-28": 4884, + "localized-paywall-e901-2024-04-04": 5441, + "price-tester-1b8e-2024-04-04": 4122, + "products-v3-60c5-2024-04-04": 4985, + "superwall-template-9f9f-2024-05-21": 4478, + "url-paywall-v3-3810-2024-04-04": 4630, + "video-v3-72b4-2024-04-04": 3513 }, - "preloadCompleteEventMs": 1278, + "preloadCompleteEventMs": 1152, "run": 9 }, { - "allReadyMs": 6223, + "allReadyMs": 4940, "cold": true, - "configureMs": 1722, + "configureMs": 1820, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 6223, - "data-v3-0ed9-2024-04-04": 5919, - "free-trial-v3-d8d7-2024-04-04": 4466, - "inimai---open-app-copy-c50c-2024-11-28": 4008, - "localized-paywall-e901-2024-04-04": 5088, - "price-tester-1b8e-2024-04-04": 5456, - "products-v3-60c5-2024-04-04": 4986, - "superwall-template-9f9f-2024-05-21": 4212, - "url-paywall-v3-3810-2024-04-04": 5240, - "video-v3-72b4-2024-04-04": 5393 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 4028, + "data-v3-0ed9-2024-04-04": 4940, + "free-trial-v3-d8d7-2024-04-04": 3774, + "inimai---open-app-copy-c50c-2024-11-28": 3113, + "localized-paywall-e901-2024-04-04": 4231, + "price-tester-1b8e-2024-04-04": 2962, + "products-v3-60c5-2024-04-04": 4180, + "superwall-template-9f9f-2024-05-21": 4585, + "url-paywall-v3-3810-2024-04-04": 3521, + "video-v3-72b4-2024-04-04": 3266 }, - "preloadCompleteEventMs": 1276, + "preloadCompleteEventMs": 594, "run": 10 } ], "schemaVersion": 1, "stats": { - "coefficientOfVariationPct": 34.216062455209986, - "coldMeanMs": 10529.8, - "maxMs": 15509, - "meanMs": 10529.8, - "medianMs": 11142.5, - "minMs": 5826, + "coefficientOfVariationPct": 32.806642844462296, + "coldMeanMs": 6732.3, + "maxMs": 12422, + "meanMs": 6732.3, + "medianMs": 6361.0, + "minMs": 4940, "sampleCount": 10, - "stdDevMs": 3602.8829444087014, + "stdDevMs": 2208.641616217735, "warmMeanMs": null }, "tier": "HIGH" diff --git a/.benchmark/results/preload-benchmark-low.json b/.benchmark/results/preload-benchmark-low.json index 373728420..541d9a435 100644 --- a/.benchmark/results/preload-benchmark-low.json +++ b/.benchmark/results/preload-benchmark-low.json @@ -11,226 +11,226 @@ "runCount": 10, "samples": [ { - "allReadyMs": 16811, + "allReadyMs": 13684, "cold": true, - "configureMs": 2093, + "configureMs": 2443, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 7472, - "data-v3-0ed9-2024-04-04": 9567, - "free-trial-v3-d8d7-2024-04-04": 12711, - "inimai---open-app-copy-c50c-2024-11-28": 6243, - "localized-paywall-e901-2024-04-04": 16811, - "price-tester-1b8e-2024-04-04": 6653, - "products-v3-60c5-2024-04-04": 6856, - "superwall-template-9f9f-2024-05-21": 5939, - "url-paywall-v3-3810-2024-04-04": 10456, - "video-v3-72b4-2024-04-04": 11153 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 10485, + "data-v3-0ed9-2024-04-04": 12796, + "free-trial-v3-d8d7-2024-04-04": 7871, + "inimai---open-app-copy-c50c-2024-11-28": 7255, + "localized-paywall-e901-2024-04-04": 9406, + "price-tester-1b8e-2024-04-04": 8867, + "products-v3-60c5-2024-04-04": 13684, + "superwall-template-9f9f-2024-05-21": 7044, + "url-paywall-v3-3810-2024-04-04": 11109, + "video-v3-72b4-2024-04-04": 9868 }, - "preloadCompleteEventMs": 1130, + "preloadCompleteEventMs": 2338, "run": 1 }, { - "allReadyMs": 8375, + "allReadyMs": 10019, "cold": true, - "configureMs": 2501, + "configureMs": 3148, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 7706, - "data-v3-0ed9-2024-04-04": 8375, - "free-trial-v3-d8d7-2024-04-04": 5979, - "inimai---open-app-copy-c50c-2024-11-28": 4495, - "localized-paywall-e901-2024-04-04": 6641, - "price-tester-1b8e-2024-04-04": 5777, - "products-v3-60c5-2024-04-04": 6336, - "superwall-template-9f9f-2024-05-21": 5161, - "url-paywall-v3-3810-2024-04-04": 5671, - "video-v3-72b4-2024-04-04": 6134 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 9311, + "data-v3-0ed9-2024-04-04": 9715, + "free-trial-v3-d8d7-2024-04-04": 8268, + "inimai---open-app-copy-c50c-2024-11-28": 6535, + "localized-paywall-e901-2024-04-04": 10019, + "price-tester-1b8e-2024-04-04": 6273, + "products-v3-60c5-2024-04-04": 8577, + "superwall-template-9f9f-2024-05-21": 7194, + "url-paywall-v3-3810-2024-04-04": 8425, + "video-v3-72b4-2024-04-04": 7501 }, - "preloadCompleteEventMs": 1293, + "preloadCompleteEventMs": 1807, "run": 2 }, { - "allReadyMs": 7902, + "allReadyMs": 7768, "cold": true, - "configureMs": 2208, + "configureMs": 2110, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 6675, - "data-v3-0ed9-2024-04-04": 7260, - "free-trial-v3-d8d7-2024-04-04": 6371, - "inimai---open-app-copy-c50c-2024-11-28": 5291, - "localized-paywall-e901-2024-04-04": 5139, - "price-tester-1b8e-2024-04-04": 6164, - "products-v3-60c5-2024-04-04": 7902, - "superwall-template-9f9f-2024-05-21": 5962, - "url-paywall-v3-3810-2024-04-04": 4886, - "video-v3-72b4-2024-04-04": 5038 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 7415, + "data-v3-0ed9-2024-04-04": 7768, + "free-trial-v3-d8d7-2024-04-04": 6259, + "inimai---open-app-copy-c50c-2024-11-28": 4413, + "localized-paywall-e901-2024-04-04": 6006, + "price-tester-1b8e-2024-04-04": 6465, + "products-v3-60c5-2024-04-04": 5594, + "superwall-template-9f9f-2024-05-21": 5027, + "url-paywall-v3-3810-2024-04-04": 6410, + "video-v3-72b4-2024-04-04": 6621 }, - "preloadCompleteEventMs": 1123, + "preloadCompleteEventMs": 1442, "run": 3 }, { - "allReadyMs": 12097, + "allReadyMs": 7715, "cold": true, - "configureMs": 1819, + "configureMs": 1625, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 11119, - "data-v3-0ed9-2024-04-04": 10091, - "free-trial-v3-d8d7-2024-04-04": 8099, - "inimai---open-app-copy-c50c-2024-11-28": 7013, - "localized-paywall-e901-2024-04-04": 7585, - "price-tester-1b8e-2024-04-04": 7167, - "products-v3-60c5-2024-04-04": 11529, - "superwall-template-9f9f-2024-05-21": 6646, - "url-paywall-v3-3810-2024-04-04": 12097, - "video-v3-72b4-2024-04-04": 11687 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 6230, + "data-v3-0ed9-2024-04-04": 7715, + "free-trial-v3-d8d7-2024-04-04": 4750, + "inimai---open-app-copy-c50c-2024-11-28": 5257, + "localized-paywall-e901-2024-04-04": 5510, + "price-tester-1b8e-2024-04-04": 4851, + "products-v3-60c5-2024-04-04": 5003, + "superwall-template-9f9f-2024-05-21": 7303, + "url-paywall-v3-3810-2024-04-04": 4239, + "video-v3-72b4-2024-04-04": 5712 }, - "preloadCompleteEventMs": 1427, + "preloadCompleteEventMs": 974, "run": 4 }, { - "allReadyMs": 5757, + "allReadyMs": 8867, "cold": true, - "configureMs": 2020, + "configureMs": 1925, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5251, - "data-v3-0ed9-2024-04-04": 5555, - "free-trial-v3-d8d7-2024-04-04": 4593, - "inimai---open-app-copy-c50c-2024-11-28": 3730, - "localized-paywall-e901-2024-04-04": 4643, - "price-tester-1b8e-2024-04-04": 3578, - "products-v3-60c5-2024-04-04": 4998, - "superwall-template-9f9f-2024-05-21": 3932, - "url-paywall-v3-3810-2024-04-04": 4846, - "video-v3-72b4-2024-04-04": 5757 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 8362, + "data-v3-0ed9-2024-04-04": 8055, + "free-trial-v3-d8d7-2024-04-04": 7649, + "inimai---open-app-copy-c50c-2024-11-28": 7093, + "localized-paywall-e901-2024-04-04": 8715, + "price-tester-1b8e-2024-04-04": 7497, + "products-v3-60c5-2024-04-04": 6532, + "superwall-template-9f9f-2024-05-21": 7346, + "url-paywall-v3-3810-2024-04-04": 8867, + "video-v3-72b4-2024-04-04": 7750 }, - "preloadCompleteEventMs": 968, + "preloadCompleteEventMs": 3777, "run": 5 }, { - "allReadyMs": 5544, + "allReadyMs": 5738, "cold": true, - "configureMs": 1762, + "configureMs": 1785, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5089, - "data-v3-0ed9-2024-04-04": 5393, - "free-trial-v3-d8d7-2024-04-04": 4380, - "inimai---open-app-copy-c50c-2024-11-28": 3722, - "localized-paywall-e901-2024-04-04": 4482, - "price-tester-1b8e-2024-04-04": 4785, - "products-v3-60c5-2024-04-04": 4684, - "superwall-template-9f9f-2024-05-21": 3975, - "url-paywall-v3-3810-2024-04-04": 4178, - "video-v3-72b4-2024-04-04": 5544 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5283, + "data-v3-0ed9-2024-04-04": 5738, + "free-trial-v3-d8d7-2024-04-04": 4119, + "inimai---open-app-copy-c50c-2024-11-28": 3511, + "localized-paywall-e901-2024-04-04": 4677, + "price-tester-1b8e-2024-04-04": 4220, + "products-v3-60c5-2024-04-04": 4321, + "superwall-template-9f9f-2024-05-21": 3713, + "url-paywall-v3-3810-2024-04-04": 3967, + "video-v3-72b4-2024-04-04": 4930 }, - "preloadCompleteEventMs": 919, + "preloadCompleteEventMs": 817, "run": 6 }, { - "allReadyMs": 6297, + "allReadyMs": 6679, "cold": true, - "configureMs": 1583, + "configureMs": 1902, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 6297, - "data-v3-0ed9-2024-04-04": 4121, - "free-trial-v3-d8d7-2024-04-04": 5132, - "inimai---open-app-copy-c50c-2024-11-28": 3614, - "localized-paywall-e901-2024-04-04": 4930, - "price-tester-1b8e-2024-04-04": 4476, - "products-v3-60c5-2024-04-04": 3817, - "superwall-template-9f9f-2024-05-21": 4728, - "url-paywall-v3-3810-2024-04-04": 4273, - "video-v3-72b4-2024-04-04": 4425 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 6224, + "data-v3-0ed9-2024-04-04": 6679, + "free-trial-v3-d8d7-2024-04-04": 5769, + "inimai---open-app-copy-c50c-2024-11-28": 4650, + "localized-paywall-e901-2024-04-04": 5870, + "price-tester-1b8e-2024-04-04": 4397, + "products-v3-60c5-2024-04-04": 4346, + "superwall-template-9f9f-2024-05-21": 4903, + "url-paywall-v3-3810-2024-04-04": 5360, + "video-v3-72b4-2024-04-04": 6376 }, - "preloadCompleteEventMs": 648, + "preloadCompleteEventMs": 1359, "run": 7 }, { - "allReadyMs": 6292, + "allReadyMs": 5555, "cold": true, - "configureMs": 1666, + "configureMs": 1623, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 4724, - "data-v3-0ed9-2024-04-04": 6292, - "free-trial-v3-d8d7-2024-04-04": 5129, - "inimai---open-app-copy-c50c-2024-11-28": 4116, - "localized-paywall-e901-2024-04-04": 3712, - "price-tester-1b8e-2024-04-04": 3560, - "products-v3-60c5-2024-04-04": 4977, - "superwall-template-9f9f-2024-05-21": 3965, - "url-paywall-v3-3810-2024-04-04": 5281, - "video-v3-72b4-2024-04-04": 4876 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5555, + "data-v3-0ed9-2024-04-04": 4797, + "free-trial-v3-d8d7-2024-04-04": 3937, + "inimai---open-app-copy-c50c-2024-11-28": 5252, + "localized-paywall-e901-2024-04-04": 4088, + "price-tester-1b8e-2024-04-04": 4038, + "products-v3-60c5-2024-04-04": 4544, + "superwall-template-9f9f-2024-05-21": 5049, + "url-paywall-v3-3810-2024-04-04": 4291, + "video-v3-72b4-2024-04-04": 4392 }, - "preloadCompleteEventMs": 1037, + "preloadCompleteEventMs": 973, "run": 8 }, { - "allReadyMs": 5386, + "allReadyMs": 6490, "cold": true, - "configureMs": 1714, + "configureMs": 1726, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5386, - "data-v3-0ed9-2024-04-04": 5083, - "free-trial-v3-d8d7-2024-04-04": 4222, - "inimai---open-app-copy-c50c-2024-11-28": 3918, - "localized-paywall-e901-2024-04-04": 4374, - "price-tester-1b8e-2024-04-04": 3665, - "products-v3-60c5-2024-04-04": 4019, - "superwall-template-9f9f-2024-05-21": 3463, - "url-paywall-v3-3810-2024-04-04": 3817, - "video-v3-72b4-2024-04-04": 3615 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 6490, + "data-v3-0ed9-2024-04-04": 5985, + "free-trial-v3-d8d7-2024-04-04": 5277, + "inimai---open-app-copy-c50c-2024-11-28": 4313, + "localized-paywall-e901-2024-04-04": 4465, + "price-tester-1b8e-2024-04-04": 4364, + "products-v3-60c5-2024-04-04": 4872, + "superwall-template-9f9f-2024-05-21": 4111, + "url-paywall-v3-3810-2024-04-04": 4973, + "video-v3-72b4-2024-04-04": 4668 }, - "preloadCompleteEventMs": 686, + "preloadCompleteEventMs": 1097, "run": 9 }, { - "allReadyMs": 6069, + "allReadyMs": 6062, "cold": true, - "configureMs": 1835, + "configureMs": 1465, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 6069, - "data-v3-0ed9-2024-04-04": 5664, - "free-trial-v3-d8d7-2024-04-04": 4296, - "inimai---open-app-copy-c50c-2024-11-28": 4397, - "localized-paywall-e901-2024-04-04": 4803, - "price-tester-1b8e-2024-04-04": 4955, - "products-v3-60c5-2024-04-04": 5107, - "superwall-template-9f9f-2024-05-21": 4043, - "url-paywall-v3-3810-2024-04-04": 5361, - "video-v3-72b4-2024-04-04": 5260 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5857, + "data-v3-0ed9-2024-04-04": 5452, + "free-trial-v3-d8d7-2024-04-04": 4896, + "inimai---open-app-copy-c50c-2024-11-28": 4238, + "localized-paywall-e901-2024-04-04": 6062, + "price-tester-1b8e-2024-04-04": 4137, + "products-v3-60c5-2024-04-04": 5604, + "superwall-template-9f9f-2024-05-21": 4491, + "url-paywall-v3-3810-2024-04-04": 5047, + "video-v3-72b4-2024-04-04": 5149 }, - "preloadCompleteEventMs": 1087, + "preloadCompleteEventMs": 1025, "run": 10 } ], "schemaVersion": 1, "stats": { - "coefficientOfVariationPct": 45.716351453666384, - "coldMeanMs": 8053.0, - "maxMs": 16811, - "meanMs": 8053.0, - "medianMs": 6294.5, - "minMs": 5386, + "coefficientOfVariationPct": 31.729301370688717, + "coldMeanMs": 7857.7, + "maxMs": 13684, + "meanMs": 7857.7, + "medianMs": 7197.0, + "minMs": 5555, "sampleCount": 10, - "stdDevMs": 3681.5377825637543, + "stdDevMs": 2493.1933138046074, "warmMeanMs": null }, "tier": "LOW" diff --git a/.benchmark/results/preload-benchmark-mid.json b/.benchmark/results/preload-benchmark-mid.json index 816feccfd..a2b1a7e17 100644 --- a/.benchmark/results/preload-benchmark-mid.json +++ b/.benchmark/results/preload-benchmark-mid.json @@ -11,226 +11,226 @@ "runCount": 10, "samples": [ { - "allReadyMs": 13314, + "allReadyMs": 14525, "cold": true, - "configureMs": 2873, + "configureMs": 3672, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 13111, - "data-v3-0ed9-2024-04-04": 11863, - "free-trial-v3-d8d7-2024-04-04": 10640, - "inimai---open-app-copy-c50c-2024-11-28": 7453, - "localized-paywall-e901-2024-04-04": 13314, - "price-tester-1b8e-2024-04-04": 8589, - "products-v3-60c5-2024-04-04": 11098, - "superwall-template-9f9f-2024-05-21": 8064, - "url-paywall-v3-3810-2024-04-04": 9570, - "video-v3-72b4-2024-04-04": 9209 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 8123, + "data-v3-0ed9-2024-04-04": 9318, + "free-trial-v3-d8d7-2024-04-04": 14525, + "inimai---open-app-copy-c50c-2024-11-28": 6515, + "localized-paywall-e901-2024-04-04": 13857, + "price-tester-1b8e-2024-04-04": 6261, + "products-v3-60c5-2024-04-04": 7024, + "superwall-template-9f9f-2024-05-21": 10570, + "url-paywall-v3-3810-2024-04-04": 9734, + "video-v3-72b4-2024-04-04": 8480 }, - "preloadCompleteEventMs": 2495, + "preloadCompleteEventMs": 1266, "run": 1 }, { - "allReadyMs": 13903, + "allReadyMs": 7657, "cold": true, - "configureMs": 2186, + "configureMs": 1416, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 12373, - "data-v3-0ed9-2024-04-04": 13903, - "free-trial-v3-d8d7-2024-04-04": 6413, - "inimai---open-app-copy-c50c-2024-11-28": 9334, - "localized-paywall-e901-2024-04-04": 12575, - "price-tester-1b8e-2024-04-04": 6721, - "products-v3-60c5-2024-04-04": 8053, - "superwall-template-9f9f-2024-05-21": 10112, - "url-paywall-v3-3810-2024-04-04": 7028, - "video-v3-72b4-2024-04-04": 11755 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 7402, + "data-v3-0ed9-2024-04-04": 6842, + "free-trial-v3-d8d7-2024-04-04": 7657, + "inimai---open-app-copy-c50c-2024-11-28": 4610, + "localized-paywall-e901-2024-04-04": 5877, + "price-tester-1b8e-2024-04-04": 6131, + "products-v3-60c5-2024-04-04": 5420, + "superwall-template-9f9f-2024-05-21": 4964, + "url-paywall-v3-3810-2024-04-04": 6487, + "video-v3-72b4-2024-04-04": 7046 }, - "preloadCompleteEventMs": 1688, + "preloadCompleteEventMs": 1850, "run": 2 }, { - "allReadyMs": 9111, + "allReadyMs": 6774, "cold": true, - "configureMs": 1767, + "configureMs": 1607, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 4320, - "data-v3-0ed9-2024-04-04": 7839, - "free-trial-v3-d8d7-2024-04-04": 8093, - "inimai---open-app-copy-c50c-2024-11-28": 5948, - "localized-paywall-e901-2024-04-04": 9111, - "price-tester-1b8e-2024-04-04": 8246, - "products-v3-60c5-2024-04-04": 9010, - "superwall-template-9f9f-2024-05-21": 6459, - "url-paywall-v3-3810-2024-04-04": 8552, - "video-v3-72b4-2024-04-04": 8757 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 6317, + "data-v3-0ed9-2024-04-04": 6774, + "free-trial-v3-d8d7-2024-04-04": 5809, + "inimai---open-app-copy-c50c-2024-11-28": 5090, + "localized-paywall-e901-2024-04-04": 5505, + "price-tester-1b8e-2024-04-04": 5657, + "products-v3-60c5-2024-04-04": 5962, + "superwall-template-9f9f-2024-05-21": 4988, + "url-paywall-v3-3810-2024-04-04": 6470, + "video-v3-72b4-2024-04-04": 6063 }, - "preloadCompleteEventMs": 1293, + "preloadCompleteEventMs": 1472, "run": 3 }, { - "allReadyMs": 5664, + "allReadyMs": 5980, "cold": true, - "configureMs": 1787, + "configureMs": 1872, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5002, - "data-v3-0ed9-2024-04-04": 5309, - "free-trial-v3-d8d7-2024-04-04": 4087, - "inimai---open-app-copy-c50c-2024-11-28": 3526, - "localized-paywall-e901-2024-04-04": 5664, - "price-tester-1b8e-2024-04-04": 4494, - "products-v3-60c5-2024-04-04": 4646, - "superwall-template-9f9f-2024-05-21": 3780, - "url-paywall-v3-3810-2024-04-04": 4748, - "video-v3-72b4-2024-04-04": 3475 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5013, + "data-v3-0ed9-2024-04-04": 5980, + "free-trial-v3-d8d7-2024-04-04": 4097, + "inimai---open-app-copy-c50c-2024-11-28": 4709, + "localized-paywall-e901-2024-04-04": 4352, + "price-tester-1b8e-2024-04-04": 4250, + "products-v3-60c5-2024-04-04": 5166, + "superwall-template-9f9f-2024-05-21": 3586, + "url-paywall-v3-3810-2024-04-04": 3739, + "video-v3-72b4-2024-04-04": 4200 }, - "preloadCompleteEventMs": 750, + "preloadCompleteEventMs": 1127, "run": 4 }, { - "allReadyMs": 5661, + "allReadyMs": 6088, "cold": true, - "configureMs": 1821, + "configureMs": 1604, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5151, - "data-v3-0ed9-2024-04-04": 5560, - "free-trial-v3-d8d7-2024-04-04": 4543, - "inimai---open-app-copy-c50c-2024-11-28": 3729, - "localized-paywall-e901-2024-04-04": 5661, - "price-tester-1b8e-2024-04-04": 4645, - "products-v3-60c5-2024-04-04": 4796, - "superwall-template-9f9f-2024-05-21": 3983, - "url-paywall-v3-3810-2024-04-04": 4391, - "video-v3-72b4-2024-04-04": 4188 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5781, + "data-v3-0ed9-2024-04-04": 6088, + "free-trial-v3-d8d7-2024-04-04": 5014, + "inimai---open-app-copy-c50c-2024-11-28": 4302, + "localized-paywall-e901-2024-04-04": 5369, + "price-tester-1b8e-2024-04-04": 4658, + "products-v3-60c5-2024-04-04": 4811, + "superwall-template-9f9f-2024-05-21": 4556, + "url-paywall-v3-3810-2024-04-04": 4201, + "video-v3-72b4-2024-04-04": 5521 }, - "preloadCompleteEventMs": 918, + "preloadCompleteEventMs": 1153, "run": 5 }, { - "allReadyMs": 6134, + "allReadyMs": 5303, "cold": true, - "configureMs": 1168, + "configureMs": 2237, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5171, - "data-v3-0ed9-2024-04-04": 6134, - "free-trial-v3-d8d7-2024-04-04": 4407, - "inimai---open-app-copy-c50c-2024-11-28": 3797, - "localized-paywall-e901-2024-04-04": 5322, - "price-tester-1b8e-2024-04-04": 5474, - "products-v3-60c5-2024-04-04": 4715, - "superwall-template-9f9f-2024-05-21": 4000, - "url-paywall-v3-3810-2024-04-04": 4204, - "video-v3-72b4-2024-04-04": 4511 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5303, + "data-v3-0ed9-2024-04-04": 5100, + "free-trial-v3-d8d7-2024-04-04": 3981, + "inimai---open-app-copy-c50c-2024-11-28": 4694, + "localized-paywall-e901-2024-04-04": 4032, + "price-tester-1b8e-2024-04-04": 3321, + "products-v3-60c5-2024-04-04": 4286, + "superwall-template-9f9f-2024-05-21": 3778, + "url-paywall-v3-3810-2024-04-04": 4490, + "video-v3-72b4-2024-04-04": 4388 }, - "preloadCompleteEventMs": 1045, + "preloadCompleteEventMs": 829, "run": 6 }, { - "allReadyMs": 6248, + "allReadyMs": 5828, "cold": true, - "configureMs": 2667, + "configureMs": 1769, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 6248, - "data-v3-0ed9-2024-04-04": 5436, - "free-trial-v3-d8d7-2024-04-04": 4620, - "inimai---open-app-copy-c50c-2024-11-28": 4058, - "localized-paywall-e901-2024-04-04": 4979, - "price-tester-1b8e-2024-04-04": 4415, - "products-v3-60c5-2024-04-04": 5639, - "superwall-template-9f9f-2024-05-21": 4262, - "url-paywall-v3-3810-2024-04-04": 5131, - "video-v3-72b4-2024-04-04": 5740 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 4915, + "data-v3-0ed9-2024-04-04": 5828, + "free-trial-v3-d8d7-2024-04-04": 4152, + "inimai---open-app-copy-c50c-2024-11-28": 4457, + "localized-paywall-e901-2024-04-04": 4254, + "price-tester-1b8e-2024-04-04": 4711, + "products-v3-60c5-2024-04-04": 5118, + "superwall-template-9f9f-2024-05-21": 3898, + "url-paywall-v3-3810-2024-04-04": 3593, + "video-v3-72b4-2024-04-04": 5219 }, - "preloadCompleteEventMs": 1156, + "preloadCompleteEventMs": 1094, "run": 7 }, { - "allReadyMs": 6569, + "allReadyMs": 6084, "cold": true, - "configureMs": 1540, + "configureMs": 1906, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5655, - "data-v3-0ed9-2024-04-04": 5250, - "free-trial-v3-d8d7-2024-04-04": 5402, - "inimai---open-app-copy-c50c-2024-11-28": 4032, - "localized-paywall-e901-2024-04-04": 6569, - "price-tester-1b8e-2024-04-04": 4387, - "products-v3-60c5-2024-04-04": 4641, - "superwall-template-9f9f-2024-05-21": 4234, - "url-paywall-v3-3810-2024-04-04": 3930, - "video-v3-72b4-2024-04-04": 4539 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 4611, + "data-v3-0ed9-2024-04-04": 6084, + "free-trial-v3-d8d7-2024-04-04": 5170, + "inimai---open-app-copy-c50c-2024-11-28": 3697, + "localized-paywall-e901-2024-04-04": 5220, + "price-tester-1b8e-2024-04-04": 4104, + "products-v3-60c5-2024-04-04": 4967, + "superwall-template-9f9f-2024-05-21": 3901, + "url-paywall-v3-3810-2024-04-04": 4358, + "video-v3-72b4-2024-04-04": 4205 }, - "preloadCompleteEventMs": 1177, + "preloadCompleteEventMs": 1022, "run": 8 }, { - "allReadyMs": 5157, + "allReadyMs": 5352, "cold": true, - "configureMs": 1777, + "configureMs": 1733, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 4599, - "data-v3-0ed9-2024-04-04": 4903, - "free-trial-v3-d8d7-2024-04-04": 5055, - "inimai---open-app-copy-c50c-2024-11-28": 3329, - "localized-paywall-e901-2024-04-04": 5157, - "price-tester-1b8e-2024-04-04": 3939, - "products-v3-60c5-2024-04-04": 4244, - "superwall-template-9f9f-2024-05-21": 3531, - "url-paywall-v3-3810-2024-04-04": 4091, - "video-v3-72b4-2024-04-04": 4295 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 4946, + "data-v3-0ed9-2024-04-04": 5352, + "free-trial-v3-d8d7-2024-04-04": 4541, + "inimai---open-app-copy-c50c-2024-11-28": 3777, + "localized-paywall-e901-2024-04-04": 4338, + "price-tester-1b8e-2024-04-04": 3421, + "products-v3-60c5-2024-04-04": 4186, + "superwall-template-9f9f-2024-05-21": 3675, + "url-paywall-v3-3810-2024-04-04": 5047, + "video-v3-72b4-2024-04-04": 4693 }, - "preloadCompleteEventMs": 572, + "preloadCompleteEventMs": 1132, "run": 9 }, { - "allReadyMs": 6017, + "allReadyMs": 5564, "cold": true, - "configureMs": 1848, + "configureMs": 1165, "index": 0, "paywallCount": 10, "perPaywallReadyMs": { - "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 6017, - "data-v3-0ed9-2024-04-04": 5663, - "free-trial-v3-d8d7-2024-04-04": 3941, - "inimai---open-app-copy-c50c-2024-11-28": 4296, - "localized-paywall-e901-2024-04-04": 4955, - "price-tester-1b8e-2024-04-04": 4600, - "products-v3-60c5-2024-04-04": 5258, - "superwall-template-9f9f-2024-05-21": 4144, - "url-paywall-v3-3810-2024-04-04": 5106, - "video-v3-72b4-2024-04-04": 5410 + "based-on-teleprompter-bigvu-template-2b72-2024-04-04": 5055, + "data-v3-0ed9-2024-04-04": 5564, + "free-trial-v3-d8d7-2024-04-04": 4238, + "inimai---open-app-copy-c50c-2024-11-28": 4085, + "localized-paywall-e901-2024-04-04": 4339, + "price-tester-1b8e-2024-04-04": 3679, + "products-v3-60c5-2024-04-04": 4799, + "superwall-template-9f9f-2024-05-21": 3933, + "url-paywall-v3-3810-2024-04-04": 4645, + "video-v3-72b4-2024-04-04": 4543 }, - "preloadCompleteEventMs": 1240, + "preloadCompleteEventMs": 1089, "run": 10 } ], "schemaVersion": 1, "stats": { - "coefficientOfVariationPct": 41.85237071417446, - "coldMeanMs": 7777.8, - "maxMs": 13903, - "meanMs": 7777.8, - "medianMs": 6191.0, - "minMs": 5157, + "coefficientOfVariationPct": 39.974661479048976, + "coldMeanMs": 6915.5, + "maxMs": 14525, + "meanMs": 6915.5, + "medianMs": 6032.0, + "minMs": 5303, "sampleCount": 10, - "stdDevMs": 3255.1936894070614, + "stdDevMs": 2764.447714583632, "warmMeanMs": null }, "tier": "MID" diff --git a/.github/workflows/preload-benchmark.yml b/.github/workflows/preload-benchmark.yml index a1f972a0b..32f606cf1 100644 --- a/.github/workflows/preload-benchmark.yml +++ b/.github/workflows/preload-benchmark.yml @@ -1,4 +1,5 @@ -# Paywall preload benchmark — runs on PRs into main or develop. +# Paywall preload benchmark — opt-in only. Runs on PRs into main or develop +# that carry the `release` or `benchmark` label, or on manual dispatch. # # Measures the time from Superwall.preloadAllPaywalls() until every paywall for # the dev app's embedded API key reaches PaywallLoadingState.Ready, on 3 @@ -19,14 +20,16 @@ name: Paywall Preload Benchmark on: - # PRs into main (release) and develop (feature) both run the benchmark, so - # preload regressions surface before they land anywhere. + # PRs into main (release) and develop (feature) run the benchmark only when + # opted in with the `release` or `benchmark` label — it burns 3 emulators for + # up to 40 minutes, so it's not worth paying on every PR push. `labeled` is in + # the trigger list so adding the label to an open PR starts the run. pull_request: branches: [ main, develop ] - # Merges re-run the benchmark AND roll the baseline forward automatically, - # so release PRs always gate against the latest mainline numbers. - push: - branches: [ main, develop ] + types: [ opened, synchronize, reopened, labeled ] + # No push trigger: a merge into main/develop carries no labels, so it could + # never satisfy the opt-in. Roll the baseline forward with a manual dispatch + # (update_baseline: true) off the branch you want to become the new mainline. workflow_dispatch: inputs: update_baseline: @@ -43,6 +46,13 @@ jobs: build-apks: runs-on: ubuntu-latest timeout-minutes: 20 + # Opt-in guard: PRs need the `release` or `benchmark` label; manual + # dispatches always run. The downstream jobs `needs` this one, so they skip + # with it. + if: >- + github.event_name == 'workflow_dispatch' || + contains(github.event.pull_request.labels.*.name, 'release') || + contains(github.event.pull_request.labels.*.name, 'benchmark') steps: - name: Checkout code @@ -254,7 +264,7 @@ jobs: --config .benchmark/config.json \ --report .benchmark/REPORT.md \ --normalized-out .benchmark/results \ - ${{ (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.update_baseline)) && '--update-baseline' || '' }} + ${{ (github.event_name == 'workflow_dispatch' && inputs.update_baseline) && '--update-baseline' || '' }} echo "exit_code=$?" >> "$GITHUB_OUTPUT" - name: Publish report to job summary diff --git a/CHANGELOG.md b/CHANGELOG.md index c8880d382..01677ef49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ The changelog for `Superwall`. Also see the [releases](https://github.com/superwall/Superwall-Android/releases) on GitHub. +## Unreleased + +## Fixes +- Fix a dead buy button when re-presenting a cached paywall in the same session after a purchase, for both `register()` and embedded paywalls (`getPaywall`/`getPaywallView`). Per-presentation transient state (loading spinner and presentation-prepared flag) is now reset on each new presentation, so it no longer leaks from a previous presentation that was stopped without a finishing teardown. + ## 2.7.21 ## Enhancements diff --git a/app/src/androidTest/java/com/example/superapp/test/PaywallHostFragment.kt b/app/src/androidTest/java/com/example/superapp/test/PaywallHostFragment.kt new file mode 100644 index 000000000..a06ac463e --- /dev/null +++ b/app/src/androidTest/java/com/example/superapp/test/PaywallHostFragment.kt @@ -0,0 +1,82 @@ +package com.example.superapp.test + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import com.superwall.sdk.paywall.presentation.get_paywall.builder.PaywallBuilder +import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult +import com.superwall.sdk.paywall.view.PaywallView +import com.superwall.sdk.paywall.view.delegate.PaywallViewCallback +import kotlinx.coroutines.launch + +/** + * Minimal test-host screen that EMBEDS a paywall inside a [Fragment] via the public + * embed API ([PaywallBuilder] -> [com.superwall.sdk.Superwall.getPaywall]), NOT via + * [com.superwall.sdk.paywall.presentation.register]. + */ +class PaywallHostFragment : Fragment() { + // Parameterised so the whole flow keys off a single placement constant. + var placement: String = RepresentTests.CONSUMABLE_REBUY_PLACEMENT + + private lateinit var container: FrameLayout + private var paywallView: PaywallView? = null + + // Minimal embed delegate. The fragment host stays on screen so the test can + // re-embed the same paywall, so we intentionally do NOT finish the activity here. + private val delegate = + object : PaywallViewCallback { + override fun onFinished( + paywall: PaywallView, + result: PaywallResult, + shouldDismiss: Boolean, + ) { + // no-op: keep the host alive for re-embedding. + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View = FrameLayout(requireContext()).also { this.container = it } + + // NOTE: no auto-present on view-create. The SDK's config fetch is gated on the app + // being in the FOREGROUND (Network.getConfig -> awaitUntilAppInForeground), so the + // test must launch this host first, await Configured, and only then call [present]. + + /** + * Obtains the embedded paywall via the public [PaywallBuilder]/`getPaywall` API and + * attaches it into the fragment's container. On the second call the SDK reuses the + * CACHED `PaywallView` (via `PaywallManager.getPaywallView`'s cache-hit branch), + * which is precisely the path whose stale `LoadingPurchase` overlay used to swallow + * the buy tap before PR #434. + */ + fun present() { + viewLifecycleOwner.lifecycleScope.launch { + embedPaywall() + } + } + + private suspend fun embedPaywall() { + // Detach any previously-embedded instance before re-embedding the (cached) view. + paywallView?.let { (it.parent as? ViewGroup)?.removeView(it) } + + PaywallBuilder(placement) + .delegate(delegate) + .activity(requireActivity()) + .build() + .onSuccess { view -> + paywallView = view + // `build()` already ran getPaywall() (cache-hit reset) + beforeViewCreated(); + // ensure the view isn't still parented, attach it, then drive onViewCreated() + // as PaywallComposable does. + (view.parent as? ViewGroup)?.removeView(view) + container.addView(view) + view.onViewCreated() + } + } +} diff --git a/app/src/androidTest/java/com/example/superapp/test/RepresentTests.kt b/app/src/androidTest/java/com/example/superapp/test/RepresentTests.kt new file mode 100644 index 000000000..8a38b561a --- /dev/null +++ b/app/src/androidTest/java/com/example/superapp/test/RepresentTests.kt @@ -0,0 +1,333 @@ +package com.example.superapp.test + +import android.app.Application +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.Until +import com.example.superapp.utils.awaitUntilWebviewAppears +import com.superwall.sdk.Superwall +import com.superwall.sdk.analytics.superwall.SuperwallEvent +import com.superwall.sdk.analytics.superwall.SuperwallEventInfo +import com.superwall.sdk.config.models.ConfigurationStatus +import com.superwall.sdk.config.options.PaywallOptions +import com.superwall.sdk.config.options.SuperwallOptions +import com.superwall.sdk.delegate.SuperwallDelegate +import com.superwall.sdk.logger.LogLevel +import com.superwall.sdk.models.entitlements.SubscriptionStatus +import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState +import com.superwall.sdk.store.testmode.TestModeBehavior +import com.superwall.superapp.Keys +import com.superwall.superapp.test.PaywallHostActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +@RunWith(AndroidJUnit4::class) +class RepresentTests { + companion object { + /** + * Placement that must present a NON-gated paywall backed by a CONSUMABLE + * product (one that grants NO persistent entitlement), so the same paywall + * can be re-presented and re-bought within a single session. + * + * `non_recurring_product` is the closest existing placement (see + * `UITestHandler.testAndroid20Info`, "Non-recurring product purchase"). If it + * turns out to be gated / to grant a blocking entitlement, point this constant + * at a dedicated consumable re-buy placement instead. + */ + const val CONSUMABLE_REBUY_PLACEMENT = "non_recurring_product" + + /** + * The paywall's primary purchase CTA label (WebView text). Must be DISTINCT + * from the test-mode activation modal's "Continue" button, otherwise the buy + * tap is ambiguous. Update to match the configured paywall's button text. + */ + const val BUY_BUTTON_TEXT = "Continue" + + // Test-mode purchase drawer confirm button (set in TestModePurchaseDrawer). + private const val CONFIRM_PURCHASE_TEXT = "Confirm Purchase" + + // Unique substring of the test-mode activation modal (its title row reads + // "✏️ Test Mode Active"), used to detect the modal without colliding with + // paywall/webview text. + private const val TEST_MODE_MODAL_HINT = "Test Mode Active" + + private val EVENT_TIMEOUT = 30.seconds + private val UI_TIMEOUT_MS = 30_000L + } + + // Live event stream fed by the Superwall delegate installed in [setup]. + private val events = MutableSharedFlow(replay = 64, extraBufferCapacity = 256) + + @Before + fun setup() { + Superwall.configure( + getInstrumentation().targetContext.applicationContext as Application, + Keys.CONSTANT_API_KEY, + options = + SuperwallOptions().apply { + logging.level = LogLevel.debug + // Billing is unavailable on CI emulators — ALWAYS routes purchases + // through the in-app test-mode drawer instead of Google Play. + testModeBehavior = TestModeBehavior.ALWAYS + paywalls = + PaywallOptions().apply { + shouldPreload = false + } + }, + ) + Superwall.instance.delegate = + object : SuperwallDelegate { + override fun handleSuperwallEvent(eventInfo: SuperwallEventInfo) { + events.tryEmit(eventInfo.event) + } + } + } + + /** + * Test 1 — repro + fix, end-to-end (embedded path). + * + * Embed → buy → drawer → Confirm → complete → re-embed the SAME placement → + * tap buy AGAIN. Pre-fix the second tap is dead (stale overlay swallows it) so the + * drawer never reappears; post-fix the purchase flow is re-invoked because the + * cache-hit reset in `PaywallManager` cleared the stale `LoadingPurchase` overlay. + */ + @Test + fun test_represent_after_consumable_purchase_reinvokes_purchase() = + runBlocking { + // Launch the host FIRST: the SDK only fetches config once the app is in the + // foreground (Network.getConfig gates on awaitUntilAppInForeground), so waiting + // for Configured before showing an activity deadlocks. + val scenario = launchHost() + awaitConfigured() + + // First embedded presentation. + scenario.presentPaywall() + dismissTestModeActivationModalIfPresent() + assertTrue("First paywall never appeared", awaitUntilWebviewAppears()) + delay(1.seconds) + + // First purchase. + tapText(BUY_BUTTON_TEXT) + assertTrue( + "Purchase drawer did not appear on the FIRST buy tap", + awaitTextAppears(CONFIRM_PURCHASE_TEXT), + ) + val firstComplete = awaitEventAsync { it is SuperwallEvent.TransactionComplete } + tapText(CONFIRM_PURCHASE_TEXT) + assertNotNull("First purchase never completed", firstComplete.await()) + // The embedded paywall isn't dismissed (no full-screen activity to finish); + // just wait for the test-mode drawer to close so the re-tap detects a FRESH drawer. + awaitTextDisappears(CONFIRM_PURCHASE_TEXT) + delay(1.seconds) + + // Re-obtain/re-attach the SAME placement via the embedded API (reuses the cached + // PaywallView through PaywallManager.getPaywallView's cache-hit reset). + scenario.presentPaywall() + dismissTestModeActivationModalIfPresent() + assertTrue("Re-presented paywall never appeared", awaitUntilWebviewAppears()) + delay(1.seconds) + + // Tap buy AGAIN — dead pre-fix, alive post-fix (PR #434). + tapText(BUY_BUTTON_TEXT) + assertTrue( + "Re-presented paywall's buy tap did NOT re-invoke the purchase flow — " + + "stale transient presentation state regression (PR #434)", + awaitTextAppears(CONFIRM_PURCHASE_TEXT), + ) + } + + /** + * Test 2 — public-API state assertion (analog of the internal-reset unit test). + * + * After purchasing and re-embedding, the re-presented paywall must NOT be stuck in + * `LoadingPurchase` (which is what showed the tap-swallowing overlay). `:app:` can't + * see the internal reset API, so we assert via the public `loadingState` getter. + */ + @Test + fun test_represent_resets_loading_state() = + runBlocking { + // Foreground-first, same as test 1 (config fetch is gated on foreground). + val scenario = launchHost() + awaitConfigured() + + scenario.presentPaywall() + dismissTestModeActivationModalIfPresent() + assertTrue("First paywall never appeared", awaitUntilWebviewAppears()) + delay(1.seconds) + + // Purchase once so a LoadingPurchase state exists to (previously) leak. + tapText(BUY_BUTTON_TEXT) + assertTrue( + "Purchase drawer did not appear on the buy tap", + awaitTextAppears(CONFIRM_PURCHASE_TEXT), + ) + val complete = awaitEventAsync { it is SuperwallEvent.TransactionComplete } + tapText(CONFIRM_PURCHASE_TEXT) + assertNotNull("Purchase never completed", complete.await()) + awaitTextDisappears(CONFIRM_PURCHASE_TEXT) + delay(1.seconds) + + // Re-obtain/re-attach the cached embedded paywall (exercises the cache-hit reset). + scenario.presentPaywall() + dismissTestModeActivationModalIfPresent() + assertTrue("Re-presented paywall never appeared", awaitUntilWebviewAppears()) + delay(1.seconds) + + val paywallView = Superwall.instance.paywallView + assertNotNull("No paywallView after re-present", paywallView) + val loadingState = paywallView!!.loadingState + // Post-fix `resetTransientPresentationState()` puts LoadingPurchase back to Ready, + // so the tap-swallowing overlay is not shown. + assertTrue( + "Re-presented paywall carried a stale LoadingPurchase state (PR #434); was $loadingState", + loadingState !is PaywallLoadingState.LoadingPurchase, + ) + } + + // region helpers + + private suspend fun awaitConfigured() { + // Force a DEFINITE entitlement status up front. On CI emulators Google Play Billing + // is unavailable (developer error 5), so the status otherwise stays `Unknown` and + // paywall presentation is SKIPPED with `subscription_status_timeout` (the status + // stayed "unknown" for >5s) — including the implicit app_install attempt that fires + // during configure. `Inactive` means "no entitlements" so the non-gated paywall + // still presents. Mirrors every paywall-presenting test in `UITestHandler`. + Superwall.instance.setSubscriptionStatus(SubscriptionStatus.Inactive) + // Bounded wait: a hang here must fail loudly with the actual state, not stall the + // suite forever (configurationStateListener never emits Configured if config load + // fails or wedges — `first {}` would otherwise suspend indefinitely). + val configured = + withTimeoutOrNull(EVENT_TIMEOUT) { + Superwall.instance.configurationStateListener.first { it is ConfigurationStatus.Configured } + } + assertNotNull( + "Superwall never reached Configured within $EVENT_TIMEOUT; " + + "last state = ${Superwall.instance.configurationState}", + configured, + ) + } + + /** + * Launches the debug-source-set host activity (it must live in the app APK so + * ActivityScenario can launch it into the app's process — see the KDoc on + * [PaywallHostActivity]) and commits [PaywallHostFragment] into its container, + * keeping all test logic in this source set. + */ + private fun launchHost(): ActivityScenario { + val scenario = ActivityScenario.launch(PaywallHostActivity::class.java) + scenario.onActivity { + it.supportFragmentManager + .beginTransaction() + .replace(PaywallHostActivity.CONTAINER_ID, PaywallHostFragment()) + .commitNow() + } + return scenario + } + + private fun ActivityScenario.presentPaywall() { + onActivity { + val fragment = + it.supportFragmentManager.findFragmentById(PaywallHostActivity.CONTAINER_ID) + (fragment as PaywallHostFragment).present() + } + } + + private fun device() = UiDevice.getInstance(getInstrumentation()) + + /** + * The ALWAYS test-mode activation modal auto-presents once (per just-activated + * session) and is non-cancelable (TestModeModal sets cancelable=false). Its + * "Continue" button sits BELOW THE FOLD of the collapsed bottom sheet, so after + * detecting the modal by its title, swipe the sheet up until the button is on + * screen, then tap it. Best-effort: absent modal is not an error. + */ + private suspend fun dismissTestModeActivationModalIfPresent() { + val d = device() + val modal = d.wait(Until.findObject(By.textContains(TEST_MODE_MODAL_HINT)), 10_000) + if (modal != null) { + var continueButton = d.findObject(By.text("Continue")) + var swipes = 0 + while (continueButton == null && swipes < 6) { + d.swipe( + d.displayWidth / 2, + (d.displayHeight * 0.85).toInt(), + d.displayWidth / 2, + (d.displayHeight * 0.25).toInt(), + 15, + ) + d.waitForIdle() + continueButton = d.findObject(By.text("Continue")) + swipes++ + } + assertNotNull("Test-mode modal is up but its Continue button never appeared", continueButton) + continueButton!!.click() + d.waitForIdle() + delay(500.milliseconds) + } + } + + private fun awaitTextAppears( + text: String, + timeoutMs: Long = UI_TIMEOUT_MS, + ): Boolean = device().wait(Until.findObject(By.textContains(text)), timeoutMs) != null + + /** + * Waits for [text] (webview or native) and taps it. Uses the By/Until API — the + * legacy [UiSelector]-based [clickButtonWith] takes a one-shot accessibility + * snapshot that can miss webview content. On failure, dumps the accessibility + * hierarchy to logcat so the actual on-screen tree is visible in CI logs. + */ + private fun tapText( + text: String, + timeoutMs: Long = UI_TIMEOUT_MS, + ) { + val obj = device().wait(Until.findObject(By.textContains(text)), timeoutMs) + if (obj == null) { + val dump = java.io.ByteArrayOutputStream() + device().dumpWindowHierarchy(dump) + println("!! tapText(\"$text\") FAILED. Accessibility hierarchy:\n$dump") + } + assertNotNull("Tap target \"$text\" never appeared within ${timeoutMs}ms", obj) + obj!!.click() + } + + /** + * Waits until [text] is no longer on screen (best-effort, bounded by [timeoutMs]). + * Used after confirming a test-mode purchase so the subsequent buy re-tap detects a + * FRESH drawer rather than the lingering previous one. The embedded paywall itself is + * never dismissed (there's no full-screen activity to finish), so we can't wait on the + * webview disappearing. + */ + private fun awaitTextDisappears( + text: String, + timeoutMs: Long = UI_TIMEOUT_MS, + ): Boolean = device().wait(Until.gone(By.textContains(text)), timeoutMs) ?: false + + /** + * Subscribe to [events] BEFORE the triggering action (avoids the emit/collect race), + * returning a Deferred that resolves to the matching event or null on timeout. + */ + private fun CoroutineScope.awaitEventAsync(predicate: (SuperwallEvent) -> Boolean) = + async(Dispatchers.IO) { + withTimeoutOrNull(EVENT_TIMEOUT) { events.first(predicate) } + } + + // endregion +} diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index fe90dc87a..018c29c4b 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -27,5 +27,12 @@ android:name="com.superwall.superapp.benchmark.BenchmarkForegroundActivity" android:exported="false" android:theme="@android:style/Theme.Material.Light" /> + + + diff --git a/app/src/debug/java/com/superwall/superapp/test/PaywallHostActivity.kt b/app/src/debug/java/com/superwall/superapp/test/PaywallHostActivity.kt new file mode 100644 index 000000000..f09d9f5d7 --- /dev/null +++ b/app/src/debug/java/com/superwall/superapp/test/PaywallHostActivity.kt @@ -0,0 +1,26 @@ +package com.superwall.superapp.test + +import android.os.Bundle +import android.view.View +import android.widget.FrameLayout +import androidx.fragment.app.FragmentActivity + +/** + * Bare fragment host for the `RepresentTests` androidTest suite. Debug-source-set only — + * ships in no release build and pulls in no test libraries. Like + * [com.superwall.superapp.benchmark.BenchmarkForegroundActivity], it must live in the app + * (not the test APK) so ActivityScenario can launch it into the app's process. + * + * The activity is deliberately empty: the test commits its own fragment into + * [CONTAINER_ID], keeping all test logic in the androidTest source set. + */ +class PaywallHostActivity : FragmentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(FrameLayout(this).apply { id = CONTAINER_ID }) + } + + companion object { + val CONTAINER_ID = View.generateViewId() + } +} diff --git a/superwall/src/androidTest/java/com/superwall/sdk/paywall/view/PaywallViewDismissTest.kt b/superwall/src/androidTest/java/com/superwall/sdk/paywall/view/PaywallViewDismissTest.kt index 10a16a422..63f6309ea 100644 --- a/superwall/src/androidTest/java/com/superwall/sdk/paywall/view/PaywallViewDismissTest.kt +++ b/superwall/src/androidTest/java/com/superwall/sdk/paywall/view/PaywallViewDismissTest.kt @@ -16,6 +16,7 @@ import com.superwall.sdk.paywall.presentation.internal.PresentationRequestType import com.superwall.sdk.paywall.presentation.internal.request.PresentationInfo import com.superwall.sdk.paywall.presentation.internal.state.PaywallResult import com.superwall.sdk.paywall.presentation.internal.state.PaywallState +import com.superwall.sdk.paywall.view.delegate.PaywallLoadingState import com.superwall.sdk.paywall.view.delegate.PaywallViewCallback import com.superwall.sdk.paywall.view.delegate.PaywallViewDelegateAdapter import kotlinx.coroutines.Dispatchers @@ -157,6 +158,60 @@ class PaywallViewDismissTest { } } + @Test + fun dismiss_purchased_resets_loading_spinner_for_embedded_paywall() = + runBlocking { + val finished = kotlinx.coroutines.CompletableDeferred() + // Mirrors the tab-bar / embedded host: onFinished is a no-op, so the view stays + // on screen after the purchase and is NEVER torn down (destroy() is not called). + val callback = + object : PaywallViewCallback { + override fun onFinished( + paywall: PaywallView, + result: PaywallResult, + shouldDismiss: Boolean, + ) { + finished.complete(Unit) + } + } + retainedCallback = callback + val delegate = PaywallViewDelegateAdapter(callback) + val view = makeView(delegate) + + val publisher = MutableSharedFlow(replay = 1, extraBufferCapacity = 1) + val request = makeRequest() + Given("an embedded paywall showing the purchase spinner") { + withContext(Dispatchers.Main) { + view.set(request, publisher, null) + view.onViewCreated() + // Buy tap sets this in production (Superwall.kt InitiatePurchase). + view.updateState( + PaywallViewState.Updates.SetLoadingState(PaywallLoadingState.LoadingPurchase), + ) + } + + When("the purchase completes and the SDK auto-dismisses") { + withContext(Dispatchers.Main) { + view.dismiss( + result = PaywallResult.Purchased(productId = "product1"), + closeReason = PaywallCloseReason.SystemLogic, + ) + } + + // Wait for the dismissal hand-off (onFinished, shouldDismiss=true). + withContext(Dispatchers.IO) { + withTimeout(3000) { finished.await() } + } + + Then("the loading spinner is reset to Ready even though the host kept the view") { + // No destroy()/teardown here: the host (tab-bar embed) leaves the view + // on screen. Pre-fix the LoadingPurchase spinner leaked and spun forever. + assertEquals(PaywallLoadingState.Ready, view.loadingState) + } + } + } + } + @Test fun dismiss_declined_for_next_paywall_does_not_clear_publisher() = runBlocking { diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallManager.kt b/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallManager.kt index 33dbb0a1f..bba42bb30 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallManager.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallManager.kt @@ -80,6 +80,19 @@ class PaywallManager( if (!isPreloading) { view.callback = delegate view.updateState(PaywallViewState.Updates.MergePaywall(it)) + // A cached view is being handed back for a new presentation. Clear any + // per-presentation transient state (a stale LoadingPurchase/ManualLoading + // spinner that swallows taps, and a stale presentationDidFinishPrepare) + // leaked by a previous presentation that stopped without a finishing + // teardown. This covers both the register()/full-screen path and the + // embedded getPaywallView/getPaywall path, which both funnel through here. + // Guarded by !isPreloading so preloading never resets a live view, and by + // isForPresentation because getPresentationResult() (a pure query API) + // also fetches through here — a result check while this paywall is + // on screen mid-purchase must not wipe its live spinner or prepare flags. + if (isForPresentation) { + view.resetTransientPresentationState() + } } return@mapAsync view } diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt index ca5c6d93b..c0cad2da9 100644 --- a/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt +++ b/superwall/src/main/java/com/superwall/sdk/paywall/view/PaywallView.kt @@ -477,6 +477,26 @@ class PaywallView( controller.updateState(ResetPresentationPreparations) } + // A cached view can be re-presented after a previous presentation was stopped without a + // finishing teardown - the purchase billing sheet (or backgrounding) triggers a non-finishing + // onStop, which since #431 intentionally no longer tears the view down. That leaks + // per-presentation transient state into the next present: a stale LoadingPurchase/ManualLoading + // spinner swallows taps, and a stale presentationDidFinishPrepare makes onViewCreated() + // early-return without re-wiring the view. Called from PaywallManager.getPaywallView's cache-hit + // branch when a cached view is handed back for a new presentation, so every new presentation - + // full-screen register() and embedded getPaywallView/getPaywall alike - gets a clean slate. + // Safe because that branch only runs for a genuinely new presentation request - never on + // resume-same-instance (that goes through onResume -> onViewCreated) nor during an in-flight + // purchase. + internal fun resetTransientPresentationState() { + if (loadingState is PaywallLoadingState.LoadingPurchase || + loadingState is PaywallLoadingState.ManualLoading + ) { + controller.updateState(SetLoadingState(PaywallLoadingState.Ready)) + } + resetPresentationPreparations() + } + internal fun dismiss( result: PaywallResult, closeReason: PaywallCloseReason, @@ -536,6 +556,15 @@ class PaywallView( } callback?.let { + // A callback-backed (embedded) paywall delegates teardown to its host, which + // may legitimately keep the view on screen (e.g. a tab-bar embed). The purchase + // spinner is otherwise only reset in the full destroy() teardown, so without + // this an embedded paywall that stays visible after a purchase spins forever. + if (loadingState is PaywallLoadingState.LoadingPurchase || + loadingState is PaywallLoadingState.ManualLoading + ) { + controller.updateState(SetLoadingState(PaywallLoadingState.Ready)) + } controller.updateState(CallbackInvoked) it.onFinished( paywall = this, diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/manager/PaywallManagerTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/manager/PaywallManagerTest.kt index 5e5a7fd73..3e5a4667e 100644 --- a/superwall/src/test/java/com/superwall/sdk/paywall/manager/PaywallManagerTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/paywall/manager/PaywallManagerTest.kt @@ -159,6 +159,89 @@ class PaywallManagerTest { verify { mockView.updateState(any()) } } + @Test + fun test_getPaywallView_resetsTransientState_onCacheHitForNewPresentation() = + runTest { + // A cached view handed back for a new presentation (register() or embedded + // getPaywallView/getPaywall) must have its per-presentation transient state cleared + // so a stale LoadingPurchase spinner / presentationDidFinishPrepare flag from a + // previous non-finishing stop can't dead-button the buy button on re-present. + val paywall = + mockk { + every { identifier } returns "test_paywall" + } + val request = + mockk { + every { isDebuggerLaunched } returns false + } + val mockView = + mockk(relaxed = true) { + every { loadingState } returns PaywallLoadingState.Unknown + } + val delegate = mockk() + + coEvery { paywallRequestManager.getPaywall(any(), any()) } returns Either.Success(paywall) + every { cache.getPaywallView(any()) } returns mockView + every { mockView.callback = any() } just Runs + every { mockView.updateState(any()) } just Runs + every { mockView.resetTransientPresentationState() } just Runs + + paywallManager.getPaywallView(request, isForPresentation = true, isPreloading = false, delegate) + + verify { mockView.resetTransientPresentationState() } + } + + @Test + fun test_getPaywallView_doesNotResetTransientState_forPresentationResultChecks() = + runTest { + // getPresentationResult() (a pure query API) fetches through getPaywallView with + // isPreloading = false but isForPresentation = false. A result check must never + // reset a live cached view's transient state (e.g. a spinner mid-purchase). + val paywall = + mockk { + every { identifier } returns "test_paywall" + } + val request = + mockk { + every { isDebuggerLaunched } returns false + } + val mockView = mockk(relaxed = true) + val delegate = mockk() + + coEvery { paywallRequestManager.getPaywall(any(), any()) } returns Either.Success(paywall) + every { cache.getPaywallView(any()) } returns mockView + every { mockView.callback = any() } just Runs + every { mockView.updateState(any()) } just Runs + + paywallManager.getPaywallView(request, isForPresentation = false, isPreloading = false, delegate) + + verify(exactly = 0) { mockView.resetTransientPresentationState() } + } + + @Test + fun test_getPaywallView_doesNotResetTransientState_whenPreloading() = + runTest { + // Preloading must never reset a (possibly live) cached view's transient state. + val paywall = + mockk { + every { identifier } returns "test_paywall" + } + val request = + mockk { + every { isDebuggerLaunched } returns false + } + val mockView = mockk(relaxed = true) + + coEvery { paywallRequestManager.getPaywall(any(), any()) } returns Either.Success(paywall) + every { cache.getPaywallView(any()) } returns mockView + + paywallManager.getPaywallView(request, isForPresentation = false, isPreloading = true, null) + + verify(exactly = 0) { mockView.resetTransientPresentationState() } + verify(exactly = 0) { mockView.callback = any() } + verify(exactly = 0) { mockView.updateState(any()) } + } + @Test fun test_getPaywallView_skipsCache_whenDebuggerLaunched() = runTest { diff --git a/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallViewTest.kt b/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallViewTest.kt index dd4daf0f1..6a95ac06d 100644 --- a/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallViewTest.kt +++ b/superwall/src/test/java/com/superwall/sdk/paywall/view/PaywallViewTest.kt @@ -928,6 +928,54 @@ class PaywallViewTest { } } + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun resetTransientPresentationState_clearsStalePurchaseSpinnerAndPrepareFlag_soRepresentReWires() = + runTest { + val dispatcher = StandardTestDispatcher(testScheduler) + Dispatchers.setMain(dispatcher) + try { + Given("a cached view left with stale transient state after a non-finishing stop mid-purchase") { + val view = makePaywallView(cache = null) + // A finished presentation whose purchase spinner never got torn down (#431): + view.controller.updateState(PaywallViewState.Updates.SetPresentedAndFinished) + view.controller.updateState( + PaywallViewState.Updates.SetLoadingState(PaywallLoadingState.LoadingPurchase), + ) + assertTrue( + "Precondition: stale LoadingPurchase spinner", + view.state.loadingState is PaywallLoadingState.LoadingPurchase, + ) + assertTrue( + "Precondition: stale presentationDidFinishPrepare", + view.state.presentationDidFinishPrepare, + ) + + When("the cached view is re-presented (PaywallManager resets transient state on cache-hit)") { + view.resetTransientPresentationState() + advanceUntilIdle() + + Then("the spinner is cleared and onViewCreated() will re-wire instead of early-returning") { + assertTrue( + "Stale purchase spinner should be reset to Ready", + view.state.loadingState is PaywallLoadingState.Ready, + ) + assertFalse( + "presentationDidFinishPrepare must be reset so onViewCreated() re-wires", + view.state.presentationDidFinishPrepare, + ) + assertTrue( + "presentationWillPrepare must be re-armed for the new presentation", + view.state.presentationWillPrepare, + ) + } + } + } + } finally { + Dispatchers.resetMain() + } + } + @OptIn(ExperimentalCoroutinesApi::class) @Test fun destroyed_whenFinishingAfterDismiss_emitsTerminalPaywallCloseWithReason_andClearsActiveKey() =