11const APP_STORE_URL = "https://apps.apple.com/app/id6753987304" ;
2+ const APP_REFERRAL_URL = "pythonide://referral" ;
23
34const copy = {
45 zh : {
@@ -11,6 +12,10 @@ const copy = {
1112 failed : "复制失败,请手动复制" ,
1213 invitedTitle : "好友邀请你使用 PythonIDE" ,
1314 invitedSummary : "下载 App 后输入邀请码,即可建立邀请关系。" ,
15+ inviteEntryTitle : "PythonIDE 邀请" ,
16+ inviteEntrySummary : "打开 App 后进入「我的」-「邀请好友」,即可查看并分享你的专属邀请码。" ,
17+ getInviteCode : "获取邀请码" ,
18+ noCodeToCopy : "当前页面没有邀请码" ,
1419 howWorks : "如何生效" ,
1520 viewRules : "查看活动规则" ,
1621 ruleTitle : "邀请活动规则" ,
@@ -51,6 +56,10 @@ const copy = {
5156 failed : "Copy failed. Please copy manually." ,
5257 invitedTitle : "A friend invited you to PythonIDE" ,
5358 invitedSummary : "Download the app and enter this invite code to bind the referral." ,
59+ inviteEntryTitle : "PythonIDE Referral" ,
60+ inviteEntrySummary : "Open the app, then go to Me - Invite Friends to find and share your personal invite code." ,
61+ getInviteCode : "Get Invite Code" ,
62+ noCodeToCopy : "No invite code on this page." ,
5463 howWorks : "How it counts" ,
5564 viewRules : "View rules" ,
5665 ruleTitle : "Referral Program Rules" ,
@@ -102,15 +111,20 @@ function routePath() {
102111
103112function inviteCodeFromPath ( ) {
104113 const raw = routePath ( ) ;
105- const url = new URL ( raw , location . origin ) ;
114+ let url ;
115+ try {
116+ url = new URL ( raw , location . origin ) ;
117+ } catch {
118+ return "" ;
119+ }
106120 const parts = url . pathname . split ( "/" ) . filter ( Boolean ) ;
107121 const code = parts [ 0 ] === "i" && parts [ 1 ] ? parts [ 1 ] : url . searchParams . get ( "code" ) ;
108- return normalizeCode ( code || "PY8K29" ) ;
122+ return normalizeCode ( code ) ;
109123}
110124
111125function normalizeCode ( value ) {
112126 const cleaned = String ( value || "" ) . replace ( / [ \s - ] + / g, "" ) . toUpperCase ( ) ;
113- return / ^ [ A - Z 0 - 9 ] { 4 , 12 } $ / . test ( cleaned ) ? cleaned : "PY8K29 " ;
127+ return / ^ [ A - Z 0 - 9 ] { 4 , 12 } $ / . test ( cleaned ) ? cleaned : "" ;
114128}
115129
116130function initialLanguage ( ) {
@@ -121,6 +135,10 @@ function initialLanguage() {
121135 return navigator . language && navigator . language . toLowerCase ( ) . startsWith ( "en" ) ? "en" : "zh" ;
122136}
123137
138+ function currentLanguage ( ) {
139+ return document . documentElement . dataset . lang === "en" ? "en" : "zh" ;
140+ }
141+
124142function setLanguage ( lang ) {
125143 const active = lang === "en" ? "en" : "zh" ;
126144 localStorage . setItem ( "referral_lang" , active ) ;
@@ -140,8 +158,7 @@ function setLanguage(lang) {
140158 document . querySelectorAll ( "[data-copy-feedback]" ) . forEach ( ( node ) => {
141159 node . dataset . copyFeedback = t . copied ;
142160 } ) ;
143- const code = document . querySelector ( "[data-invite-code]" ) ?. dataset . inviteCode || inviteCodeFromPath ( ) ;
144- renderInviteCode ( code ) ;
161+ applyInvitePageState ( inviteCodeFromPath ( ) , t ) ;
145162 setupReveal ( ) ;
146163 setupPointerMotion ( ) ;
147164}
@@ -208,15 +225,69 @@ function svgIcon(name) {
208225function renderInviteCode ( code ) {
209226 const target = document . querySelector ( "[data-invite-code]" ) ;
210227 if ( ! target ) return ;
228+ if ( ! code ) {
229+ renderInvitePlaceholder ( copy [ currentLanguage ( ) ] . getInviteCode ) ;
230+ return ;
231+ }
211232 target . dataset . inviteCode = code ;
212233 target . dataset . length = String ( code . length ) ;
234+ target . classList . remove ( "invite-placeholder" ) ;
213235 target . classList . toggle ( "compact" , code . length >= 7 ) ;
214236 target . classList . toggle ( "ultra-compact" , code . length >= 10 ) ;
237+ target . removeAttribute ( "role" ) ;
238+ target . removeAttribute ( "tabindex" ) ;
239+ target . setAttribute ( "aria-label" , `${ copy [ currentLanguage ( ) ] . invalidCode } ${ code } ` ) ;
215240 target . innerHTML = code . split ( "" ) . map ( ( char , index ) => `<span style="--i:${ index } ">${ char } </span>` ) . join ( "" ) ;
216241}
217242
243+ function renderInvitePlaceholder ( label ) {
244+ const target = document . querySelector ( "[data-invite-code]" ) ;
245+ if ( ! target ) return ;
246+ target . dataset . inviteCode = "" ;
247+ target . dataset . length = "0" ;
248+ target . classList . remove ( "compact" , "ultra-compact" ) ;
249+ target . classList . add ( "invite-placeholder" ) ;
250+ target . setAttribute ( "role" , "button" ) ;
251+ target . setAttribute ( "tabindex" , "0" ) ;
252+ target . setAttribute ( "aria-label" , label ) ;
253+ target . textContent = label ;
254+ }
255+
256+ function applyInvitePageState ( code , t ) {
257+ const target = document . querySelector ( "[data-invite-code]" ) ;
258+ if ( ! target ) {
259+ document . body . classList . remove ( "invite-missing-code" ) ;
260+ return ;
261+ }
262+
263+ const hasCode = Boolean ( code ) ;
264+ document . body . classList . toggle ( "invite-missing-code" , ! hasCode ) ;
265+
266+ const title = document . getElementById ( "invite-title" ) ;
267+ if ( title ) title . textContent = hasCode ? t . invitedTitle : t . inviteEntryTitle ;
268+
269+ const summary = document . querySelector ( "[data-invite-summary]" ) ;
270+ if ( summary ) summary . textContent = hasCode ? t . invitedSummary : t . inviteEntrySummary ;
271+
272+ const copyButton = document . querySelector ( "[data-copy-code]" ) ;
273+ if ( copyButton ) copyButton . hidden = ! hasCode ;
274+
275+ const rulesButton = document . querySelector ( "[data-no-code-rules]" ) ;
276+ if ( rulesButton ) rulesButton . hidden = hasCode ;
277+
278+ if ( hasCode ) {
279+ renderInviteCode ( code ) ;
280+ } else {
281+ renderInvitePlaceholder ( t . getInviteCode ) ;
282+ }
283+ }
284+
218285async function copyInviteCode ( ) {
219286 const code = document . querySelector ( "[data-invite-code]" ) ?. dataset . inviteCode || inviteCodeFromPath ( ) ;
287+ if ( ! code ) {
288+ showToast ( copy [ currentLanguage ( ) ] . noCodeToCopy ) ;
289+ return ;
290+ }
220291 try {
221292 await copyText ( code ) ;
222293 document . body . classList . add ( "code-copied" ) ;
@@ -232,6 +303,30 @@ async function copyInviteCode() {
232303 }
233304}
234305
306+ function openReferralInApp ( ) {
307+ const startedAt = Date . now ( ) ;
308+ let didHide = false ;
309+ const fallbackDelay = 900 ;
310+ const onVisibilityChange = ( ) => {
311+ if ( document . hidden ) didHide = true ;
312+ } ;
313+
314+ document . addEventListener ( "visibilitychange" , onVisibilityChange , { once : true } ) ;
315+ window . setTimeout ( ( ) => {
316+ document . removeEventListener ( "visibilitychange" , onVisibilityChange ) ;
317+ if ( ! didHide && Date . now ( ) - startedAt < 1800 ) {
318+ window . location . href = APP_STORE_URL ;
319+ }
320+ } , fallbackDelay ) ;
321+
322+ window . location . href = APP_REFERRAL_URL ;
323+ }
324+
325+ function activateInvitePlaceholder ( ) {
326+ if ( ! document . body . classList . contains ( "invite-missing-code" ) ) return ;
327+ openReferralInApp ( ) ;
328+ }
329+
235330async function copyText ( text ) {
236331 if ( navigator . clipboard && window . isSecureContext ) {
237332 await navigator . clipboard . writeText ( text ) ;
@@ -316,10 +411,19 @@ function setupPointerMotion() {
316411}
317412
318413function init ( ) {
319- renderInviteCode ( inviteCodeFromPath ( ) ) ;
320414 document . querySelectorAll ( "[data-download]" ) . forEach ( ( node ) => {
321415 node . setAttribute ( "href" , APP_STORE_URL ) ;
322416 } ) ;
417+ const inviteCode = document . querySelector ( "[data-invite-code]" ) ;
418+ if ( inviteCode ) {
419+ inviteCode . addEventListener ( "click" , activateInvitePlaceholder ) ;
420+ inviteCode . addEventListener ( "keydown" , ( event ) => {
421+ if ( event . key !== "Enter" && event . key !== " " ) return ;
422+ if ( ! document . body . classList . contains ( "invite-missing-code" ) ) return ;
423+ event . preventDefault ( ) ;
424+ openReferralInApp ( ) ;
425+ } ) ;
426+ }
323427 document . querySelectorAll ( "[data-copy-code]" ) . forEach ( ( node ) => {
324428 node . addEventListener ( "click" , copyInviteCode ) ;
325429 } ) ;
0 commit comments