source for getorbyt.com getorbyt.com/
client bsky orbytapp app orbyt bluesky getorbyt orbytvideo atproto video
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: integrate AltStore download options and enhance button layout

- Added dynamic handling for AltStore download options in both app and index pages.
- Introduced new components for AltStore hints and adjusted button layout for better visibility.
- Updated styles for download buttons and wrappers to improve user experience.
- Changed prerender settings to false for both pages to accommodate dynamic content.

+945 -5
public/images/altstore-pal-badge.png

This is a binary file and will not be displayed.

public/images/altstore-pal-peek-icon.png

This is a binary file and will not be displayed.

+5
public/images/altstore-pal-squircle-frame.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="-5 -5 110 110" fill="none" overflow="visible"> 2 + <g transform="translate(5,5)"> 3 + <path d="M 100.0000,50.0000 L 99.9807,40.0930 L 99.9229,35.9947 L 99.8264,32.8581 L 99.6913,30.2241 L 99.5173,27.9155 L 99.3044,25.8419 L 99.0524,23.9500 L 98.7611,22.2054 L 98.4301,20.5841 L 98.0593,19.0693 L 97.6483,17.6480 L 97.1966,16.3106 L 96.7037,15.0493 L 96.1693,13.8580 L 95.5925,12.7317 L 94.9727,11.6664 L 94.3091,10.6587 L 93.6006,9.7058 L 92.8463,8.8053 L 92.0448,7.9552 L 91.1947,7.1537 L 90.2942,6.3994 L 89.3413,5.6909 L 88.3336,5.0273 L 87.2683,4.4075 L 86.1420,3.8307 L 84.9507,3.2963 L 83.6894,2.8034 L 82.3520,2.3517 L 80.9307,1.9407 L 79.4159,1.5699 L 77.7946,1.2389 L 76.0500,0.9476 L 74.1581,0.6956 L 72.0845,0.4827 L 69.7759,0.3087 L 67.1419,0.1736 L 64.0053,0.0771 L 59.9070,0.0193 L 50.0000,0.0000 L 40.0930,0.0193 L 35.9947,0.0771 L 32.8581,0.1736 L 30.2241,0.3087 L 27.9155,0.4827 L 25.8419,0.6956 L 23.9500,0.9476 L 22.2054,1.2389 L 20.5841,1.5699 L 19.0693,1.9407 L 17.6480,2.3517 L 16.3106,2.8034 L 15.0493,3.2963 L 13.8580,3.8307 L 12.7317,4.4075 L 11.6664,5.0273 L 10.6587,5.6909 L 9.7058,6.3994 L 8.8053,7.1537 L 7.9552,7.9552 L 7.1537,8.8053 L 6.3994,9.7058 L 5.6909,10.6587 L 5.0273,11.6664 L 4.4075,12.7317 L 3.8307,13.8580 L 3.2963,15.0493 L 2.8034,16.3106 L 2.3517,17.6480 L 1.9407,19.0693 L 1.5699,20.5841 L 1.2389,22.2054 L 0.9476,23.9500 L 0.6956,25.8419 L 0.4827,27.9155 L 0.3087,30.2241 L 0.1736,32.8581 L 0.0771,35.9947 L 0.0193,40.0930 L 0.0000,50.0000 L 0.0193,59.9070 L 0.0771,64.0053 L 0.1736,67.1419 L 0.3087,69.7759 L 0.4827,72.0845 L 0.6956,74.1581 L 0.9476,76.0500 L 1.2389,77.7946 L 1.5699,79.4159 L 1.9407,80.9307 L 2.3517,82.3520 L 2.8034,83.6894 L 3.2963,84.9507 L 3.8307,86.1420 L 4.4075,87.2683 L 5.0273,88.3336 L 5.6909,89.3413 L 6.3994,90.2942 L 7.1537,91.1947 L 7.9552,92.0448 L 8.8053,92.8463 L 9.7058,93.6006 L 10.6587,94.3091 L 11.6664,94.9727 L 12.7317,95.5925 L 13.8580,96.1693 L 15.0493,96.7037 L 16.3106,97.1966 L 17.6480,97.6483 L 19.0693,98.0593 L 20.5841,98.4301 L 22.2054,98.7611 L 23.9500,99.0524 L 25.8419,99.3044 L 27.9155,99.5173 L 30.2241,99.6913 L 32.8581,99.8264 L 35.9947,99.9229 L 40.0930,99.9807 L 50.0000,100.0000 L 59.9070,99.9807 L 64.0053,99.9229 L 67.1419,99.8264 L 69.7759,99.6913 L 72.0845,99.5173 L 74.1581,99.3044 L 76.0500,99.0524 L 77.7946,98.7611 L 79.4159,98.4301 L 80.9307,98.0593 L 82.3520,97.6483 L 83.6894,97.1966 L 84.9507,96.7037 L 86.1420,96.1693 L 87.2683,95.5925 L 88.3336,94.9727 L 89.3413,94.3091 L 90.2942,93.6006 L 91.1947,92.8463 L 92.0448,92.0448 L 92.8463,91.1947 L 93.6006,90.2942 L 94.3091,89.3413 L 94.9727,88.3336 L 95.5925,87.2683 L 96.1693,86.1420 L 96.7037,84.9507 L 97.1966,83.6894 L 97.6483,82.3520 L 98.0593,80.9307 L 98.4301,79.4159 L 98.7611,77.7946 L 99.0524,76.0500 L 99.3044,74.1581 L 99.5173,72.0845 L 99.6913,69.7759 L 99.8264,67.1419 L 99.9229,64.0053 L 99.9807,59.9070 L 100.0000,50.0000 Z" fill="none" stroke="#000" stroke-width="4.5455" stroke-linejoin="round" stroke-linecap="round"/> 4 + </g> 5 + </svg>
+4
public/images/altstore-pal-squircle-mask.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> 2 + <rect width="100" height="100" fill="#000"/> 3 + <path d="M 100.0000,50.0000 L 99.9807,40.0930 L 99.9229,35.9947 L 99.8264,32.8581 L 99.6913,30.2241 L 99.5173,27.9155 L 99.3044,25.8419 L 99.0524,23.9500 L 98.7611,22.2054 L 98.4301,20.5841 L 98.0593,19.0693 L 97.6483,17.6480 L 97.1966,16.3106 L 96.7037,15.0493 L 96.1693,13.8580 L 95.5925,12.7317 L 94.9727,11.6664 L 94.3091,10.6587 L 93.6006,9.7058 L 92.8463,8.8053 L 92.0448,7.9552 L 91.1947,7.1537 L 90.2942,6.3994 L 89.3413,5.6909 L 88.3336,5.0273 L 87.2683,4.4075 L 86.1420,3.8307 L 84.9507,3.2963 L 83.6894,2.8034 L 82.3520,2.3517 L 80.9307,1.9407 L 79.4159,1.5699 L 77.7946,1.2389 L 76.0500,0.9476 L 74.1581,0.6956 L 72.0845,0.4827 L 69.7759,0.3087 L 67.1419,0.1736 L 64.0053,0.0771 L 59.9070,0.0193 L 50.0000,0.0000 L 40.0930,0.0193 L 35.9947,0.0771 L 32.8581,0.1736 L 30.2241,0.3087 L 27.9155,0.4827 L 25.8419,0.6956 L 23.9500,0.9476 L 22.2054,1.2389 L 20.5841,1.5699 L 19.0693,1.9407 L 17.6480,2.3517 L 16.3106,2.8034 L 15.0493,3.2963 L 13.8580,3.8307 L 12.7317,4.4075 L 11.6664,5.0273 L 10.6587,5.6909 L 9.7058,6.3994 L 8.8053,7.1537 L 7.9552,7.9552 L 7.1537,8.8053 L 6.3994,9.7058 L 5.6909,10.6587 L 5.0273,11.6664 L 4.4075,12.7317 L 3.8307,13.8580 L 3.2963,15.0493 L 2.8034,16.3106 L 2.3517,17.6480 L 1.9407,19.0693 L 1.5699,20.5841 L 1.2389,22.2054 L 0.9476,23.9500 L 0.6956,25.8419 L 0.4827,27.9155 L 0.3087,30.2241 L 0.1736,32.8581 L 0.0771,35.9947 L 0.0193,40.0930 L 0.0000,50.0000 L 0.0193,59.9070 L 0.0771,64.0053 L 0.1736,67.1419 L 0.3087,69.7759 L 0.4827,72.0845 L 0.6956,74.1581 L 0.9476,76.0500 L 1.2389,77.7946 L 1.5699,79.4159 L 1.9407,80.9307 L 2.3517,82.3520 L 2.8034,83.6894 L 3.2963,84.9507 L 3.8307,86.1420 L 4.4075,87.2683 L 5.0273,88.3336 L 5.6909,89.3413 L 6.3994,90.2942 L 7.1537,91.1947 L 7.9552,92.0448 L 8.8053,92.8463 L 9.7058,93.6006 L 10.6587,94.3091 L 11.6664,94.9727 L 12.7317,95.5925 L 13.8580,96.1693 L 15.0493,96.7037 L 16.3106,97.1966 L 17.6480,97.6483 L 19.0693,98.0593 L 20.5841,98.4301 L 22.2054,98.7611 L 23.9500,99.0524 L 25.8419,99.3044 L 27.9155,99.5173 L 30.2241,99.6913 L 32.8581,99.8264 L 35.9947,99.9229 L 40.0930,99.9807 L 50.0000,100.0000 L 59.9070,99.9807 L 64.0053,99.9229 L 67.1419,99.8264 L 69.7759,99.6913 L 72.0845,99.5173 L 74.1581,99.3044 L 76.0500,99.0524 L 77.7946,98.7611 L 79.4159,98.4301 L 80.9307,98.0593 L 82.3520,97.6483 L 83.6894,97.1966 L 84.9507,96.7037 L 86.1420,96.1693 L 87.2683,95.5925 L 88.3336,94.9727 L 89.3413,94.3091 L 90.2942,93.6006 L 91.1947,92.8463 L 92.0448,92.0448 L 92.8463,91.1947 L 93.6006,90.2942 L 94.3091,89.3413 L 94.9727,88.3336 L 95.5925,87.2683 L 96.1693,86.1420 L 96.7037,84.9507 L 97.1966,83.6894 L 97.6483,82.3520 L 98.0593,80.9307 L 98.4301,79.4159 L 98.7611,77.7946 L 99.0524,76.0500 L 99.3044,74.1581 L 99.5173,72.0845 L 99.6913,69.7759 L 99.8264,67.1419 L 99.9229,64.0053 L 99.9807,59.9070 L 100.0000,50.0000 Z" fill="#fff"/> 4 + </svg>
+4
public/images/arrow-altstore-pal-tip.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 151.59 76.31" aria-hidden="true"> 2 + <!-- Unmirrored art; pages use inline <svg> + CSS scaleX(-1) so flip is reliable. --> 3 + <path fill="#ffffff" d="M982.31,624.53c-5.53,1-11.18,2-16.82,3.15-23,4.61-45,12.26-66.64,21.06-8.44,3.43-16.84,7-25.25,10.46A186.9,186.9,0,0,0,852,669.61c-1,.55-2,1.07-3,1.53a1.66,1.66,0,0,1-2.06-1.43,5.84,5.84,0,0,1,2.41-5.44,21.38,21.38,0,0,1,3.28-2c4.29-2.13,8.58-4.29,12.93-6.29,6-2.74,12-5.36,17.95-8a492.84,492.84,0,0,1,60.35-21.91c8.74-2.59,17.58-4.79,26.48-6.71a13.38,13.38,0,0,0,1.37-.38,0.69,0.69,0,0,0,.32-0.3,0.49,0.49,0,0,0-.26-0.8,130.75,130.75,0,0,0-15.48-5c-7.75-2-15.43-4.15-23-6.73-2.72-.93-5.41-1.95-8.09-3a11.41,11.41,0,0,1-2.49-1.41,2.27,2.27,0,0,1-.85-2.13,5.59,5.59,0,0,1,2.29-4.05,2.67,2.67,0,0,1,2.26-.54,9,9,0,0,1,1.81.6,114.9,114.9,0,0,0,21.31,7.24q11.84,3.05,23.46,6.84c4.7,1.54,9.57,2.53,14.37,3.74,2.63,0.67,5.28,1.29,7.91,1.94a13.62,13.62,0,0,1,1.82.57,2.53,2.53,0,0,1,1.33,2.8,7.39,7.39,0,0,1-2.17,3.66,5,5,0,0,1-.76.58c-7,4-11.41,10.58-16.32,16.68-2.93,3.64-4.82,7.89-7.27,11.81a23.64,23.64,0,0,0-1.45,3c-1.61,3.65-3.18,7.31-4.81,11a4.12,4.12,0,0,1-2.25,2.38,2.11,2.11,0,0,1-1.84-.06,2.16,2.16,0,0,1-1.19-1.94,3.7,3.7,0,0,1,.23-1.41c0.68-1.79,1.37-3.58,2.12-5.34s1.56-3.16,2.17-4.8a63.42,63.42,0,0,1,3.83-7.72c3.42-6.39,7.94-12,12.33-17.68,0.78-1,1.58-2,2.34-3a1.41,1.41,0,0,0,.23-0.85A2.11,2.11,0,0,0,982.31,624.53Z" transform="translate(-846.95 -594.93)"/> 4 + </svg>
+299
src/components/AltstorePalPeek.astro
··· 1 + --- 2 + import AltstorePalTipArrow from './AltstorePalTipArrow.astro'; 3 + import '../styles/altstore-pal-peek.css'; 4 + 5 + interface Props { 6 + href: string; 7 + ariaLabel?: string; 8 + } 9 + 10 + const { href, ariaLabel = 'Add orbyt source in AltStore PAL' } = Astro.props; 11 + /** Same as `href` — AltStore PAL deep link for scanning from iPhone */ 12 + const qrCodeImageUrl = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&bgcolor=000000&color=f3f5fe&data=${encodeURIComponent(href)}`; 13 + --- 14 + 15 + <a 16 + class="altstore-pal-peek" 17 + href={href} 18 + aria-label={ariaLabel} 19 + aria-haspopup="dialog" 20 + data-altstore-pal-qr-trigger 21 + > 22 + <span class="altstore-pal-peek__tip" aria-hidden="true"> 23 + <span class="altstore-pal-peek__tip-label">get it on altstore</span> 24 + <AltstorePalTipArrow /> 25 + </span> 26 + <img 27 + class="altstore-pal-peek__img" 28 + src="/images/altstore-pal-badge.png" 29 + alt="" 30 + width="46" 31 + height="46" 32 + decoding="async" 33 + aria-hidden="true" 34 + /> 35 + </a> 36 + 37 + <dialog 38 + class="altstore-pal-qr-dialog" 39 + aria-labelledby="altstore-pal-qr-dialog-title" 40 + data-altstore-pal-qr-dialog 41 + > 42 + <div class="altstore-pal-qr-dialog__panel"> 43 + <h2 id="altstore-pal-qr-dialog-title" class="altstore-pal-qr-dialog__title"> 44 + Scan with your iPhone 45 + </h2> 46 + <figure class="altstore-pal-qr-dialog__media"> 47 + <div class="altstore-pal-qr-dialog__qr-wrap"> 48 + <img 49 + class="altstore-pal-qr-dialog__qr" 50 + src={qrCodeImageUrl} 51 + alt="QR code for AltStore PAL — scan with your iPhone" 52 + width="200" 53 + height="200" 54 + loading="lazy" 55 + decoding="async" 56 + /> 57 + </div> 58 + <figcaption class="altstore-pal-qr-dialog__hint"> 59 + Adds the orbyt source in AltStore PAL 60 + </figcaption> 61 + </figure> 62 + <form class="altstore-pal-qr-dialog__dismiss" method="dialog"> 63 + <button type="submit" class="altstore-pal-qr-dialog__close"> 64 + Close 65 + </button> 66 + </form> 67 + <p class="altstore-pal-qr-dialog__new"> 68 + New to altstore?{' '} 69 + <a 70 + class="altstore-pal-qr-dialog__altstore-link" 71 + href="https://altstore.io/#Downloads" 72 + target="_blank" 73 + rel="noopener noreferrer" 74 + > 75 + get it here 76 + </a> 77 + </p> 78 + </div> 79 + </dialog> 80 + <script is:inline> 81 + (function () { 82 + const trigger = document.querySelector('a[data-altstore-pal-qr-trigger]'); 83 + const dialog = document.querySelector('dialog[data-altstore-pal-qr-dialog]'); 84 + if (!trigger || !dialog) return; 85 + const mq = window.matchMedia('(hover: hover) and (pointer: fine)'); 86 + trigger.addEventListener('click', function (e) { 87 + if (!mq.matches) return; 88 + if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; 89 + e.preventDefault(); 90 + dialog.showModal(); 91 + }); 92 + dialog.addEventListener('click', function (e) { 93 + if (e.target === dialog) dialog.close(); 94 + }); 95 + })(); 96 + </script> 97 + 98 + <style> 99 + .altstore-pal-qr-dialog { 100 + padding: 0; 101 + border: none; 102 + background: transparent; 103 + max-width: calc(100vw - 32px); 104 + } 105 + 106 + .altstore-pal-qr-dialog:focus, 107 + .altstore-pal-qr-dialog:focus-visible { 108 + outline: none; 109 + } 110 + 111 + .altstore-pal-qr-dialog::backdrop { 112 + background-color: rgb(0 0 0 / 0%); 113 + transition: background-color 0.28s ease; 114 + } 115 + 116 + .altstore-pal-qr-dialog[open]::backdrop { 117 + background-color: rgb(0 0 0 / 72%); 118 + } 119 + 120 + @starting-style { 121 + .altstore-pal-qr-dialog[open]::backdrop { 122 + background-color: rgb(0 0 0 / 0%); 123 + } 124 + } 125 + 126 + .altstore-pal-qr-dialog__panel { 127 + background: #000; 128 + color: #f3f5fe; 129 + border: 2px solid #333; 130 + border-radius: 20px; 131 + padding: 24px 22px 18px; 132 + font-family: 133 + 'Figtree', 134 + system-ui, 135 + sans-serif; 136 + text-align: center; 137 + max-width: 300px; 138 + box-sizing: border-box; 139 + transform-origin: center center; 140 + opacity: 0; 141 + transform: scale(0.94) translateY(12px); 142 + transition: 143 + opacity 0.32s cubic-bezier(0.22, 1, 0.36, 1), 144 + transform 0.32s cubic-bezier(0.22, 1, 0.36, 1); 145 + } 146 + 147 + .altstore-pal-qr-dialog[open] .altstore-pal-qr-dialog__panel { 148 + opacity: 1; 149 + transform: scale(1) translateY(0); 150 + } 151 + 152 + @starting-style { 153 + .altstore-pal-qr-dialog[open] .altstore-pal-qr-dialog__panel { 154 + opacity: 0; 155 + transform: scale(0.94) translateY(12px); 156 + } 157 + } 158 + 159 + .altstore-pal-qr-dialog__title { 160 + margin: 0 0 14px; 161 + font-size: 1.125rem; 162 + font-weight: 600; 163 + line-height: 1.25; 164 + letter-spacing: -0.02em; 165 + } 166 + 167 + .altstore-pal-qr-dialog__media { 168 + margin: 0; 169 + padding: 0; 170 + display: flex; 171 + flex-direction: column; 172 + align-items: center; 173 + } 174 + 175 + .altstore-pal-qr-dialog__qr-wrap { 176 + background: #000; 177 + padding: 16px; 178 + border-radius: 16px; 179 + border: 2px solid #333; 180 + display: inline-block; 181 + box-sizing: border-box; 182 + } 183 + 184 + .altstore-pal-qr-dialog__qr { 185 + width: 200px; 186 + height: 200px; 187 + display: block; 188 + vertical-align: middle; 189 + } 190 + 191 + .altstore-pal-qr-dialog__hint { 192 + margin: 6px 0 0; 193 + padding: 0 2px; 194 + max-width: 28ch; 195 + font-size: 12px; 196 + font-weight: 400; 197 + font-style: normal; 198 + line-height: 1.35; 199 + color: #8f94a3; 200 + letter-spacing: 0.04em; 201 + text-wrap: balance; 202 + } 203 + 204 + .altstore-pal-qr-dialog__dismiss { 205 + display: block; 206 + width: 100%; 207 + margin: 20px 0 0; 208 + padding: 0; 209 + border: none; 210 + background: none; 211 + } 212 + 213 + .altstore-pal-qr-dialog__new { 214 + margin: 12px 0 0; 215 + font-size: 13px; 216 + font-weight: 400; 217 + line-height: 1.45; 218 + color: #787d8c; 219 + letter-spacing: 0.02em; 220 + } 221 + 222 + .altstore-pal-qr-dialog__altstore-link { 223 + color: var(--site-doc-link, var(--orbyt-teal)); 224 + text-decoration: none; 225 + transition: color 0.2s ease, opacity 0.2s ease; 226 + -webkit-tap-highlight-color: transparent; 227 + } 228 + 229 + .altstore-pal-qr-dialog__altstore-link:hover { 230 + color: var(--site-doc-link, var(--orbyt-teal)); 231 + opacity: 0.92; 232 + } 233 + 234 + .altstore-pal-qr-dialog__altstore-link:focus, 235 + .altstore-pal-qr-dialog__altstore-link:focus-visible { 236 + outline: none; 237 + box-shadow: none; 238 + } 239 + 240 + .altstore-pal-qr-dialog__close { 241 + margin-top: 0; 242 + width: 100%; 243 + box-sizing: border-box; 244 + background: #551def; 245 + border: 2px solid #000; 246 + border-radius: 30px; 247 + height: 48px; 248 + font-family: inherit; 249 + font-weight: 700; 250 + font-size: 16px; 251 + color: #e0ebff; 252 + cursor: pointer; 253 + -webkit-tap-highlight-color: transparent; 254 + } 255 + 256 + .altstore-pal-qr-dialog__close:focus, 257 + .altstore-pal-qr-dialog__close:focus-visible { 258 + outline: none; 259 + box-shadow: none; 260 + } 261 + 262 + .altstore-pal-qr-dialog__close::-moz-focus-inner { 263 + border: 0; 264 + } 265 + 266 + .altstore-pal-qr-dialog__close:hover { 267 + opacity: 0.92; 268 + } 269 + 270 + @media (max-width: 500px) { 271 + .altstore-pal-qr-dialog__qr { 272 + width: 180px; 273 + height: 180px; 274 + } 275 + } 276 + 277 + @media (prefers-reduced-motion: reduce) { 278 + .altstore-pal-qr-dialog::backdrop { 279 + transition: none; 280 + } 281 + 282 + .altstore-pal-qr-dialog[open]::backdrop { 283 + background-color: rgb(0 0 0 / 72%); 284 + } 285 + 286 + .altstore-pal-qr-dialog__panel { 287 + transition: none; 288 + opacity: 1; 289 + transform: none; 290 + } 291 + 292 + @starting-style { 293 + .altstore-pal-qr-dialog[open] .altstore-pal-qr-dialog__panel { 294 + opacity: 1; 295 + transform: none; 296 + } 297 + } 298 + } 299 + </style>
+18
src/components/AltstorePalTipArrow.astro
··· 1 + --- 2 + /* Inline SVG so CSS scaleX(-1) reliably points the head at the AltStore badge. */ 3 + --- 4 + <svg 5 + class="altstore-pal-peek__tip-arrow" 6 + xmlns="http://www.w3.org/2000/svg" 7 + viewBox="0 0 151.59 76.31" 8 + aria-hidden="true" 9 + width="24" 10 + height="12" 11 + focusable="false" 12 + > 13 + <path 14 + fill="#ffffff" 15 + d="M982.31,624.53c-5.53,1-11.18,2-16.82,3.15-23,4.61-45,12.26-66.64,21.06-8.44,3.43-16.84,7-25.25,10.46A186.9,186.9,0,0,0,852,669.61c-1,.55-2,1.07-3,1.53a1.66,1.66,0,0,1-2.06-1.43,5.84,5.84,0,0,1,2.41-5.44,21.38,21.38,0,0,1,3.28-2c4.29-2.13,8.58-4.29,12.93-6.29,6-2.74,12-5.36,17.95-8a492.84,492.84,0,0,1,60.35-21.91c8.74-2.59,17.58-4.79,26.48-6.71a13.38,13.38,0,0,0,1.37-.38,0.69,0.69,0,0,0,.32-0.3,0.49,0.49,0,0,0-.26-0.8,130.75,130.75,0,0,0-15.48-5c-7.75-2-15.43-4.15-23-6.73-2.72-.93-5.41-1.95-8.09-3a11.41,11.41,0,0,1-2.49-1.41,2.27,2.27,0,0,1-.85-2.13,5.59,5.59,0,0,1,2.29-4.05,2.67,2.67,0,0,1,2.26-.54,9,9,0,0,1,1.81.6,114.9,114.9,0,0,0,21.31,7.24q11.84,3.05,23.46,6.84c4.7,1.54,9.57,2.53,14.37,3.74,2.63,0.67,5.28,1.29,7.91,1.94a13.62,13.62,0,0,1,1.82.57,2.53,2.53,0,0,1,1.33,2.8,7.39,7.39,0,0,1-2.17,3.66,5,5,0,0,1-.76.58c-7,4-11.41,10.58-16.32,16.68-2.93,3.64-4.82,7.89-7.27,11.81a23.64,23.64,0,0,0-1.45,3c-1.61,3.65-3.18,7.31-4.81,11a4.12,4.12,0,0,1-2.25,2.38,2.11,2.11,0,0,1-1.84-.06,2.16,2.16,0,0,1-1.19-1.94,3.7,3.7,0,0,1,.23-1.41c0.68-1.79,1.37-3.58,2.12-5.34s1.56-3.16,2.17-4.8a63.42,63.42,0,0,1,3.83-7.72c3.42-6.39,7.94-12,12.33-17.68,0.78-1,1.58-2,2.34-3a1.41,1.41,0,0,0,.23-0.85A2.11,2.11,0,0,0,982.31,624.53Z" 16 + transform="translate(-846.95 -594.93)" 17 + /> 18 + </svg>
+1
src/components/AltstorePalTouchHint.astro
··· 1 + <p class="altstore-pal-hint" aria-hidden="true">Opens with AltStore PAL</p>
+40 -3
src/pages/app.astro
··· 1 1 --- 2 2 // All users see this landing page with QR code 3 + import AltstorePalPeek from '../components/AltstorePalPeek.astro'; 4 + import AltstorePalTouchHint from '../components/AltstorePalTouchHint.astro'; 3 5 import DocumentColorScheme from '../components/DocumentColorScheme.astro'; 6 + import { 7 + getIosDownloadOptionsFromRequest, 8 + iosDownloadHrefFromOptions, 9 + } from '../utils/ios-distribution'; 4 10 import { SITE_DOCUMENT } from '../utils/site-document-theme'; 5 11 12 + export const prerender = false; 13 + 6 14 const appUrl = 'https://getorbyt.com/app'; 7 15 const qrCodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&bgcolor=000000&color=f3f5fe&data=${encodeURIComponent(appUrl)}`; 16 + const iosDownload = getIosDownloadOptionsFromRequest(Astro.request); 17 + const iosHref = iosDownloadHrefFromOptions(iosDownload); 18 + const showAltstorePalPeek = iosDownload.primary === 'altstore'; 8 19 --- 9 20 <!doctype html> 10 21 <html lang="en" class="orbyt-doc-dark"> ··· 29 40 } 30 41 </style> 31 42 <link rel="preload" href="/fonts/Figtree/Figtree-VariableFont_wght.woff2" as="font" type="font/woff2" crossorigin> 32 - 43 + <link rel="preconnect" href="https://fonts.googleapis.com" /> 44 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 45 + <link href="https://fonts.googleapis.com/css2?family=Caveat:wght@400&display=swap" rel="stylesheet" /> 33 46 <link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png"> 34 47 <link rel="icon" type="image/x-icon" href="/favicon/favicon.ico"> 35 48 ··· 123 136 margin-top: 10px; 124 137 width: 100%; 125 138 max-width: 400px; 139 + overflow: visible; 140 + } 141 + .download-button-wrap { 142 + position: relative; 143 + width: 100%; 144 + margin: 0 auto 12px auto; 145 + overflow: visible; 126 146 } 147 + 148 + .download-button-wrap .download-button { 149 + position: relative; 150 + z-index: 1; 151 + margin: 0; 152 + } 153 + 127 154 .download-button { 128 155 background: #551DEF; 129 156 border: 2px solid #000000; ··· 144 171 .download-button:hover { 145 172 opacity: 0.9; 146 173 } 147 - 174 + 148 175 /* Mobile-specific text */ 149 176 .subtitle-mobile { 150 177 display: none; ··· 182 209 font-size: 18px; 183 210 } 184 211 } 212 + 185 213 </style> 186 214 </head> 187 215 <body> ··· 212 240 </div> 213 241 214 242 <div class="buttons"> 215 - <a href="/ios" class="download-button">iOS</a> 243 + <div 244 + class:list={[ 245 + 'download-button-wrap', 246 + { 'download-button-wrap--pal': showAltstorePalPeek }, 247 + ]} 248 + > 249 + {showAltstorePalPeek && <AltstorePalPeek href={iosHref} />} 250 + <a href="/ios" class="download-button">iOS</a> 251 + {showAltstorePalPeek && <AltstorePalTouchHint />} 252 + </div> 216 253 <a href="/android" class="download-button">Android Waitlist</a> 217 254 </div> 218 255 </div>
+47 -2
src/pages/index.astro
··· 1 1 --- 2 - export const prerender = true; 2 + export const prerender = false; 3 3 4 + import AltstorePalPeek from '../components/AltstorePalPeek.astro'; 5 + import AltstorePalTouchHint from '../components/AltstorePalTouchHint.astro'; 4 6 import DocumentColorScheme from '../components/DocumentColorScheme.astro'; 5 7 import SiteFooter from '../components/SiteFooter.astro'; 8 + import { 9 + getIosDownloadOptionsFromRequest, 10 + iosDownloadHrefFromOptions, 11 + } from '../utils/ios-distribution'; 6 12 import { SITE_DOCUMENT } from '../utils/site-document-theme'; 13 + 14 + const iosDownload = getIosDownloadOptionsFromRequest(Astro.request); 15 + const iosHref = iosDownloadHrefFromOptions(iosDownload); 16 + const showAltstorePalPeek = iosDownload.primary === 'altstore'; 7 17 --- 8 18 9 19 <!doctype html> ··· 23 33 <meta name="viewport" content="width=device-width, initial-scale=1"> 24 34 25 35 <link rel="preload" href="/fonts/Figtree/Figtree-VariableFont_wght.woff2" as="font" type="font/woff2" crossorigin> 36 + <link rel="preconnect" href="https://fonts.googleapis.com" /> 37 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> 38 + <link href="https://fonts.googleapis.com/css2?family=Caveat:wght@400&display=swap" rel="stylesheet" /> 26 39 27 40 <meta property="og:title" content="orbyt - video communities"/> 28 41 <meta property="og:description" content="a new video app for bluesky"/> ··· 84 97 --fg-color: var(--orbyt-white); 85 98 } 86 99 100 + .download-button-wrap { 101 + position: relative; 102 + max-width: 500px; 103 + margin: 0 auto 10px auto; 104 + overflow: visible; 105 + } 106 + 107 + .download-button-wrap .download-button { 108 + position: relative; 109 + z-index: 1; 110 + margin: 0; 111 + max-width: none; 112 + width: 100%; 113 + } 114 + 87 115 .download-button { 88 116 background: #551DEF; 89 117 border: 2px solid #000000; ··· 193 221 font-size: 36px; 194 222 } 195 223 224 + .download-button-wrap { 225 + margin-bottom: 15px; 226 + } 227 + 228 + .download-button-wrap .download-button { 229 + margin-bottom: 0; 230 + } 231 + 196 232 .download-button { 197 233 height: 50px; 198 234 font-size: 20px; ··· 226 262 <p class="motto-text">video communities</p> 227 263 </div> 228 264 <div class="home-actions"> 229 - <a href="/ios" class="download-button">iOS</a> 265 + <div 266 + class:list={[ 267 + 'download-button-wrap', 268 + { 'download-button-wrap--pal': showAltstorePalPeek }, 269 + ]} 270 + > 271 + {showAltstorePalPeek && <AltstorePalPeek href={iosHref} />} 272 + <a href="/ios" class="download-button">iOS</a> 273 + {showAltstorePalPeek && <AltstorePalTouchHint />} 274 + </div> 230 275 <a href="/android" class="download-button">Android Waitlist</a> 231 276 </div> 232 277 </main>
+418
src/styles/altstore-pal-peek.css
··· 1 + /* AltStore PAL badge + tip; parent must include .download-button-wrap--pal on the wrap */ 2 + 3 + .download-button-wrap--pal .altstore-pal-hint { 4 + display: none; 5 + margin: 0; 6 + padding: 0; 7 + font-family: 'Figtree', sans-serif; 8 + font-size: 13px; 9 + font-weight: 500; 10 + letter-spacing: 0.05em; 11 + color: #888; 12 + text-align: center; 13 + line-height: 1.4; 14 + pointer-events: none; 15 + } 16 + 17 + @media (hover: none), (pointer: coarse) { 18 + .download-button-wrap--pal .altstore-pal-hint { 19 + display: block; 20 + margin-top: 10px; 21 + } 22 + } 23 + 24 + .download-button-wrap--pal { 25 + --altstore-badge-border: 2px; 26 + --altstore-badge-clip: polygon( 27 + 100% 50%, 28 + 99.99% 58.86%, 29 + 99.95% 62.53%, 30 + 99.89% 65.34%, 31 + 99.8% 67.7%, 32 + 99.69% 69.78%, 33 + 99.56% 71.64%, 34 + 99.39% 73.35%, 35 + 99.21% 74.93%, 36 + 99% 76.41%, 37 + 98.76% 77.79%, 38 + 98.5% 79.1%, 39 + 98.21% 80.34%, 40 + 97.9% 81.51%, 41 + 97.56% 82.63%, 42 + 97.2% 83.69%, 43 + 96.81% 84.7%, 44 + 96.39% 85.67%, 45 + 95.94% 86.6%, 46 + 95.47% 87.49%, 47 + 94.97% 88.33%, 48 + 94.45% 89.14%, 49 + 93.89% 89.92%, 50 + 93.3% 90.66%, 51 + 92.69% 91.37%, 52 + 92.04% 92.04%, 53 + 91.37% 92.69%, 54 + 90.66% 93.3%, 55 + 89.92% 93.89%, 56 + 89.14% 94.45%, 57 + 88.33% 94.97%, 58 + 87.49% 95.47%, 59 + 86.6% 95.94%, 60 + 85.67% 96.39%, 61 + 84.7% 96.81%, 62 + 83.69% 97.2%, 63 + 82.63% 97.56%, 64 + 81.51% 97.9%, 65 + 80.34% 98.21%, 66 + 79.1% 98.5%, 67 + 77.79% 98.76%, 68 + 76.41% 99%, 69 + 74.93% 99.21%, 70 + 73.35% 99.39%, 71 + 71.64% 99.56%, 72 + 69.78% 99.69%, 73 + 67.7% 99.8%, 74 + 65.34% 99.89%, 75 + 62.53% 99.95%, 76 + 58.86% 99.99%, 77 + 50% 100%, 78 + 41.14% 99.99%, 79 + 37.47% 99.95%, 80 + 34.66% 99.89%, 81 + 32.3% 99.8%, 82 + 30.22% 99.69%, 83 + 28.36% 99.56%, 84 + 26.65% 99.39%, 85 + 25.07% 99.21%, 86 + 23.59% 99%, 87 + 22.21% 98.76%, 88 + 20.9% 98.5%, 89 + 19.66% 98.21%, 90 + 18.49% 97.9%, 91 + 17.37% 97.56%, 92 + 16.31% 97.2%, 93 + 15.3% 96.81%, 94 + 14.33% 96.39%, 95 + 13.4% 95.94%, 96 + 12.51% 95.47%, 97 + 11.67% 94.97%, 98 + 10.86% 94.45%, 99 + 10.08% 93.89%, 100 + 9.34% 93.3%, 101 + 8.63% 92.69%, 102 + 7.96% 92.04%, 103 + 7.31% 91.37%, 104 + 6.7% 90.66%, 105 + 6.11% 89.92%, 106 + 5.55% 89.14%, 107 + 5.03% 88.33%, 108 + 4.53% 87.49%, 109 + 4.06% 86.6%, 110 + 3.61% 85.67%, 111 + 3.19% 84.7%, 112 + 2.8% 83.69%, 113 + 2.44% 82.63%, 114 + 2.1% 81.51%, 115 + 1.79% 80.34%, 116 + 1.5% 79.1%, 117 + 1.24% 77.79%, 118 + 1% 76.41%, 119 + 0.79% 74.93%, 120 + 0.61% 73.35%, 121 + 0.44% 71.64%, 122 + 0.31% 69.78%, 123 + 0.2% 67.7%, 124 + 0.11% 65.34%, 125 + 0.05% 62.53%, 126 + 0.01% 58.86%, 127 + 0% 50%, 128 + 0.01% 41.14%, 129 + 0.05% 37.47%, 130 + 0.11% 34.66%, 131 + 0.2% 32.3%, 132 + 0.31% 30.22%, 133 + 0.44% 28.36%, 134 + 0.61% 26.65%, 135 + 0.79% 25.07%, 136 + 1% 23.59%, 137 + 1.24% 22.21%, 138 + 1.5% 20.9%, 139 + 1.79% 19.66%, 140 + 2.1% 18.49%, 141 + 2.44% 17.37%, 142 + 2.8% 16.31%, 143 + 3.19% 15.3%, 144 + 3.61% 14.33%, 145 + 4.06% 13.4%, 146 + 4.53% 12.51%, 147 + 5.03% 11.67%, 148 + 5.55% 10.86%, 149 + 6.11% 10.08%, 150 + 6.7% 9.34%, 151 + 7.31% 8.63%, 152 + 7.96% 7.96%, 153 + 8.63% 7.31%, 154 + 9.34% 6.7%, 155 + 10.08% 6.11%, 156 + 10.86% 5.55%, 157 + 11.67% 5.03%, 158 + 12.51% 4.53%, 159 + 13.4% 4.06%, 160 + 14.33% 3.61%, 161 + 15.3% 3.19%, 162 + 16.31% 2.8%, 163 + 17.37% 2.44%, 164 + 18.49% 2.1%, 165 + 19.66% 1.79%, 166 + 20.9% 1.5%, 167 + 22.21% 1.24%, 168 + 23.59% 1%, 169 + 25.07% 0.79%, 170 + 26.65% 0.61%, 171 + 28.36% 0.44%, 172 + 30.22% 0.31%, 173 + 32.3% 0.2%, 174 + 34.66% 0.11%, 175 + 37.47% 0.05%, 176 + 41.14% 0.01%, 177 + 50% 0%, 178 + 58.86% 0.01%, 179 + 62.53% 0.05%, 180 + 65.34% 0.11%, 181 + 67.7% 0.2%, 182 + 69.78% 0.31%, 183 + 71.64% 0.44%, 184 + 73.35% 0.61%, 185 + 74.93% 0.79%, 186 + 76.41% 1%, 187 + 77.79% 1.24%, 188 + 79.1% 1.5%, 189 + 80.34% 1.79%, 190 + 81.51% 2.1%, 191 + 82.63% 2.44%, 192 + 83.69% 2.8%, 193 + 84.7% 3.19%, 194 + 85.67% 3.61%, 195 + 86.6% 4.06%, 196 + 87.49% 4.53%, 197 + 88.33% 5.03%, 198 + 89.14% 5.55%, 199 + 89.92% 6.11%, 200 + 90.66% 6.7%, 201 + 91.37% 7.31%, 202 + 92.04% 7.96%, 203 + 92.69% 8.63%, 204 + 93.3% 9.34%, 205 + 93.89% 10.08%, 206 + 94.45% 10.86%, 207 + 94.97% 11.67%, 208 + 95.47% 12.51%, 209 + 95.94% 13.4%, 210 + 96.39% 14.33%, 211 + 96.81% 15.3%, 212 + 97.2% 16.31%, 213 + 97.56% 17.37%, 214 + 97.9% 18.49%, 215 + 98.21% 19.66%, 216 + 98.5% 20.9%, 217 + 98.76% 22.21%, 218 + 99% 23.59%, 219 + 99.21% 25.07%, 220 + 99.39% 26.65%, 221 + 99.56% 28.36%, 222 + 99.69% 30.22%, 223 + 99.8% 32.3%, 224 + 99.89% 34.66%, 225 + 99.95% 37.47%, 226 + 99.99% 41.14%, 227 + 100% 50% 228 + ); 229 + } 230 + 231 + .download-button-wrap--pal .altstore-pal-peek { 232 + position: absolute; 233 + z-index: 2; 234 + width: 44px; 235 + height: 44px; 236 + top: 2px; 237 + right: 6px; 238 + transform-origin: 58% 76%; 239 + transform: translate(26%, -34%) rotate(7deg); 240 + transition: transform 0.38s cubic-bezier(0.22, 1, 0.36, 1); 241 + text-decoration: none; 242 + -webkit-tap-highlight-color: transparent; 243 + } 244 + 245 + .download-button-wrap--pal .altstore-pal-peek::before { 246 + content: ''; 247 + position: absolute; 248 + z-index: 0; 249 + inset: calc(-1 * var(--altstore-badge-border)); 250 + pointer-events: none; 251 + background: var(--orbyt-black); 252 + -webkit-clip-path: var(--altstore-badge-clip); 253 + clip-path: var(--altstore-badge-clip); 254 + } 255 + 256 + .download-button-wrap--pal .altstore-pal-peek:focus-visible { 257 + outline: 2px solid #01f5b3; 258 + outline-offset: 3px; 259 + z-index: 3; 260 + } 261 + 262 + .download-button-wrap--pal .altstore-pal-peek__img { 263 + position: relative; 264 + z-index: 1; 265 + display: block; 266 + width: 100%; 267 + height: 100%; 268 + pointer-events: none; 269 + -webkit-clip-path: var(--altstore-badge-clip); 270 + clip-path: var(--altstore-badge-clip); 271 + filter: 272 + drop-shadow(0 2px 5px rgba(0, 0, 0, 0.22)) drop-shadow(0 0 18px rgba(1, 245, 179, 0.12)); 273 + transition: filter 0.38s ease; 274 + } 275 + 276 + .altstore-pal-peek__tip { 277 + display: none; 278 + } 279 + 280 + @media (hover: hover) and (pointer: fine) { 281 + .download-button-wrap--pal .altstore-pal-peek::after { 282 + content: ''; 283 + position: absolute; 284 + z-index: 3; 285 + inset: -20px; 286 + } 287 + 288 + .download-button-wrap--pal .altstore-pal-peek__tip { 289 + display: flex; 290 + flex-direction: row; 291 + align-items: center; 292 + direction: ltr; 293 + gap: 9px; 294 + position: absolute; 295 + right: 100%; 296 + top: 50%; 297 + transform: translateY(calc(-50% - 13px)); 298 + margin-right: 5px; 299 + padding: 2px 0; 300 + pointer-events: none; 301 + white-space: nowrap; 302 + opacity: 0; 303 + transition: opacity 0.2s ease; 304 + z-index: 2; 305 + } 306 + 307 + .download-button-wrap--pal .altstore-pal-peek__tip-label { 308 + font-family: 'Caveat', cursive; 309 + font-size: 0.94rem; 310 + font-weight: 400; 311 + line-height: 1; 312 + color: rgba(255, 255, 255, 0.92); 313 + letter-spacing: 0.02em; 314 + text-transform: lowercase; 315 + } 316 + 317 + .download-button-wrap--pal .altstore-pal-peek__tip-arrow { 318 + display: block; 319 + width: 24px; 320 + height: auto; 321 + flex-shrink: 0; 322 + align-self: center; 323 + margin: 2px 3px 2px 2px; 324 + opacity: 0.95; 325 + overflow: visible; 326 + transform-origin: 32% 48%; 327 + transform: rotate(30deg); 328 + } 329 + 330 + .download-button-wrap--pal .altstore-pal-peek:hover .altstore-pal-peek__tip, 331 + .download-button-wrap--pal .altstore-pal-peek:focus-visible .altstore-pal-peek__tip { 332 + opacity: 1; 333 + } 334 + 335 + .download-button-wrap--pal .altstore-pal-peek:hover, 336 + .download-button-wrap--pal .altstore-pal-peek:focus-visible { 337 + z-index: 3; 338 + transform: translate(38%, -46%) rotate(7deg) scale(1.04); 339 + } 340 + 341 + .download-button-wrap--pal .altstore-pal-peek:hover .altstore-pal-peek__img, 342 + .download-button-wrap--pal .altstore-pal-peek:focus-visible .altstore-pal-peek__img { 343 + filter: 344 + drop-shadow(0 4px 12px rgba(0, 0, 0, 0.2)) drop-shadow(0 0 26px rgba(1, 245, 179, 0.22)); 345 + } 346 + } 347 + 348 + @media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: reduce) { 349 + .download-button-wrap--pal .altstore-pal-peek__tip { 350 + transition: none; 351 + } 352 + } 353 + 354 + @media (prefers-reduced-motion: reduce) { 355 + .download-button-wrap--pal .altstore-pal-peek { 356 + transition: none; 357 + transform: translate(38%, -40%) rotate(7deg); 358 + } 359 + 360 + .download-button-wrap--pal .altstore-pal-peek__img { 361 + transition: none; 362 + filter: 363 + drop-shadow(0 2px 6px rgba(0, 0, 0, 0.2)) drop-shadow(0 0 18px rgba(1, 245, 179, 0.15)); 364 + } 365 + } 366 + 367 + @media (max-width: 500px) { 368 + .download-button-wrap--pal .altstore-pal-peek { 369 + width: 40px; 370 + height: 40px; 371 + right: 4px; 372 + } 373 + 374 + .download-button-wrap--pal .altstore-pal-hint { 375 + font-size: 12px; 376 + margin-top: 8px; 377 + } 378 + } 379 + 380 + @media (hover: none) and (pointer: coarse) { 381 + .download-button-wrap--pal .altstore-pal-peek { 382 + z-index: 2; 383 + display: flex; 384 + align-items: center; 385 + justify-content: center; 386 + width: 50px; 387 + height: 50px; 388 + top: 0; 389 + right: 0; 390 + box-sizing: border-box; 391 + box-shadow: none; 392 + } 393 + 394 + .download-button-wrap--pal .altstore-pal-peek::before { 395 + inset: auto; 396 + width: calc(38px + 2 * var(--altstore-badge-border)); 397 + height: calc(38px + 2 * var(--altstore-badge-border)); 398 + left: 50%; 399 + top: 50%; 400 + transform: translate(-50%, -50%); 401 + } 402 + 403 + .download-button-wrap--pal .altstore-pal-peek__img { 404 + width: 38px; 405 + height: 38px; 406 + max-width: 38px; 407 + max-height: 38px; 408 + flex-shrink: 0; 409 + -webkit-clip-path: var(--altstore-badge-clip); 410 + clip-path: var(--altstore-badge-clip); 411 + filter: 412 + drop-shadow(0 2px 5px rgba(0, 0, 0, 0.22)) drop-shadow(0 0 18px rgba(1, 245, 179, 0.12)); 413 + } 414 + 415 + .download-button-wrap--pal .altstore-pal-peek:active .altstore-pal-peek__img { 416 + opacity: 0.92; 417 + } 418 + }
+109
src/utils/ios-distribution.ts
··· 1 + /** 2 + * Cloudflare sets `cf-ipcountry` to an ISO 3166-1 alpha-2 code (e.g. "DE", "JP"). 3 + * See: https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#cf-ipcountry 4 + */ 5 + 6 + /** EU member states (27) — regions where AltStore PAL / alternative distribution applies per project docs. */ 7 + const EU_MEMBER_CODES = new Set([ 8 + 'AT', 9 + 'BE', 10 + 'BG', 11 + 'HR', 12 + 'CY', 13 + 'CZ', 14 + 'DK', 15 + 'EE', 16 + 'FI', 17 + 'FR', 18 + 'DE', 19 + 'GR', 20 + 'HU', 21 + 'IE', 22 + 'IT', 23 + 'LV', 24 + 'LT', 25 + 'LU', 26 + 'MT', 27 + 'NL', 28 + 'PL', 29 + 'PT', 30 + 'RO', 31 + 'SK', 32 + 'SI', 33 + 'ES', 34 + 'SE', 35 + ]); 36 + 37 + function normalizeCountryCode(value: string | null | undefined): string | null { 38 + if (value == null || value === '') return null; 39 + const code = value.trim().toUpperCase(); 40 + if (code === 'XX' || code === 'T1' || code.length !== 2) return null; 41 + return code; 42 + } 43 + 44 + function isAltstorePalRegion(cfIpCountry: string | null | undefined): boolean { 45 + const code = normalizeCountryCode(cfIpCountry); 46 + if (code == null) return false; 47 + return code === 'JP' || EU_MEMBER_CODES.has(code); 48 + } 49 + 50 + export type IosDownloadPrimary = 'altstore' | 'appstore'; 51 + 52 + export type IosDownloadOptions = Readonly<{ 53 + primary: IosDownloadPrimary; 54 + altstoreSourceUrl: string; 55 + appStoreUrl: string; 56 + }>; 57 + 58 + const DEFAULT_ALTSTORE_SOURCE = 'https://getorbyt.com/altstore/source.json'; 59 + /** Same marketplace ID as `public/altstore/source.json` */ 60 + const DEFAULT_APP_STORE = 'https://apps.apple.com/app/id6751679299'; 61 + 62 + export function getIosDownloadOptions( 63 + cfIpCountry: string | null | undefined, 64 + overrides?: Partial<Pick<IosDownloadOptions, 'altstoreSourceUrl' | 'appStoreUrl'>>, 65 + ): IosDownloadOptions { 66 + const altstoreSourceUrl = overrides?.altstoreSourceUrl ?? DEFAULT_ALTSTORE_SOURCE; 67 + const appStoreUrl = overrides?.appStoreUrl ?? DEFAULT_APP_STORE; 68 + const useAltstoreFirst = 69 + import.meta.env.DEV || isAltstorePalRegion(cfIpCountry); 70 + return { 71 + primary: useAltstoreFirst ? 'altstore' : 'appstore', 72 + altstoreSourceUrl, 73 + appStoreUrl, 74 + }; 75 + } 76 + 77 + export function getIosDownloadOptionsFromRequest( 78 + request: Request, 79 + overrides?: Partial<Pick<IosDownloadOptions, 'altstoreSourceUrl' | 'appStoreUrl'>>, 80 + ): IosDownloadOptions { 81 + return getIosDownloadOptions(request.headers.get('cf-ipcountry'), overrides); 82 + } 83 + 84 + /** 85 + * AltStore PAL (EU / Japan App Store build) registers `altstore-pal://`. 86 + * Classic sideload AltStore uses `altstore://`; this site targets PAL for primary iOS distribution. 87 + */ 88 + function altstorePalSourceDeepLink(sourceUrl: string): string { 89 + return `altstore-pal://source?url=${encodeURIComponent(sourceUrl)}`; 90 + } 91 + 92 + function iosHrefForOptions(options: IosDownloadOptions): string { 93 + return options.primary === 'altstore' 94 + ? altstorePalSourceDeepLink(options.altstoreSourceUrl) 95 + : options.appStoreUrl; 96 + } 97 + 98 + /** Resolved `href` from options (same rules as `iosDownloadHrefFromRequest`). */ 99 + export function iosDownloadHrefFromOptions(options: IosDownloadOptions): string { 100 + return iosHrefForOptions(options); 101 + } 102 + 103 + /** Resolved `href` for the primary iOS CTA (AltStore deep link vs App Store). */ 104 + export function iosDownloadHrefFromRequest( 105 + request: Request, 106 + overrides?: Partial<Pick<IosDownloadOptions, 'altstoreSourceUrl' | 'appStoreUrl'>>, 107 + ): string { 108 + return iosHrefForOptions(getIosDownloadOptionsFromRequest(request, overrides)); 109 + }