11import React , { memo , useState , useEffect } from 'react' ;
2- import { View , Text , ScrollView , Switch } from 'react-native' ;
2+ import { View , Text , ScrollView , Switch , Platform , Linking } from 'react-native' ;
33import { StyleSheet } from 'react-native-unistyles' ;
44import QRCode from 'qrcode' ;
55import { Image } from 'expo-image' ;
6+ import * as Clipboard from 'expo-clipboard' ;
67import { PublicSessionShare } from '@/sync/sharingTypes' ;
78import { Item } from '@/components/Item' ;
89import { ItemList } from '@/components/ItemList' ;
910import { RoundButton } from '@/components/RoundButton' ;
1011import { t } from '@/text' ;
11- import { getServerUrl } from '@/sync/serverConfig' ;
1212import { Ionicons } from '@expo/vector-icons' ;
13+ import { BaseModal } from '@/modal/components/BaseModal' ;
14+ import { Modal } from '@/modal' ;
1315
1416/**
1517 * Props for PublicLinkDialog component
@@ -43,21 +45,40 @@ export const PublicLinkDialog = memo(function PublicLinkDialog({
4345 onCancel
4446} : PublicLinkDialogProps ) {
4547 const [ qrDataUrl , setQrDataUrl ] = useState < string | null > ( null ) ;
48+ const [ shareUrl , setShareUrl ] = useState < string | null > ( null ) ;
4649 const [ isConfiguring , setIsConfiguring ] = useState ( false ) ;
4750 const [ expiresInDays , setExpiresInDays ] = useState < number | undefined > ( 7 ) ;
4851 const [ maxUses , setMaxUses ] = useState < number | undefined > ( undefined ) ;
4952 const [ isConsentRequired , setIsConsentRequired ] = useState ( true ) ;
5053
54+ const buildPublicShareUrl = ( token : string ) : string => {
55+ const path = `/share/${ token } ` ;
56+
57+ if ( Platform . OS === 'web' ) {
58+ const origin =
59+ typeof window !== 'undefined' && window . location ?. origin
60+ ? window . location . origin
61+ : '' ;
62+ return `${ origin } ${ path } ` ;
63+ }
64+
65+ const configuredWebAppUrl = ( process . env . EXPO_PUBLIC_HAPPY_WEBAPP_URL || '' ) . trim ( ) ;
66+ const webAppUrl = configuredWebAppUrl || 'https://app.happy.engineering' ;
67+ return `${ webAppUrl } ${ path } ` ;
68+ } ;
69+
5170 // Generate QR code when public share exists
5271 useEffect ( ( ) => {
5372 if ( ! publicShare ?. token ) {
5473 setQrDataUrl ( null ) ;
74+ setShareUrl ( null ) ;
5575 return ;
5676 }
5777
58- // Use the configured server URL to generate the share link
59- const serverUrl = getServerUrl ( ) ;
60- const url = `${ serverUrl } /share/${ publicShare . token } ` ;
78+ // IMPORTANT: Public share links point to the web app route (`/share/:token`),
79+ // not the API server URL.
80+ const url = buildPublicShareUrl ( publicShare . token ) ;
81+ setShareUrl ( url ) ;
6182
6283 QRCode . toDataURL ( url , {
6384 width : 250 ,
@@ -84,19 +105,43 @@ export const PublicLinkDialog = memo(function PublicLinkDialog({
84105 return new Date ( timestamp ) . toLocaleDateString ( ) ;
85106 } ;
86107
108+ const handleOpenLink = async ( ) => {
109+ if ( ! shareUrl ) return ;
110+ try {
111+ if ( Platform . OS === 'web' ) {
112+ window . open ( shareUrl , '_blank' , 'noopener,noreferrer' ) ;
113+ return ;
114+ }
115+ await Linking . openURL ( shareUrl ) ;
116+ } catch {
117+ // ignore
118+ }
119+ } ;
120+
121+ const handleCopyLink = async ( ) => {
122+ if ( ! shareUrl ) return ;
123+ try {
124+ await Clipboard . setStringAsync ( shareUrl ) ;
125+ Modal . alert ( t ( 'common.copied' ) , t ( 'items.copiedToClipboard' , { label : t ( 'session.sharing.publicLink' ) } ) ) ;
126+ } catch {
127+ Modal . alert ( t ( 'common.error' ) , t ( 'textSelection.failedToCopy' ) ) ;
128+ }
129+ } ;
130+
87131 return (
88- < View style = { styles . container } >
89- < View style = { styles . header } >
90- < Text style = { styles . title } > { t ( 'session.sharing.publicLink' ) } </ Text >
91- < Item
92- title = { t ( 'common.cancel' ) }
93- onPress = { onCancel }
94- />
95- </ View >
132+ < BaseModal visible = { true } onClose = { onCancel } >
133+ < View style = { styles . container } >
134+ < View style = { styles . header } >
135+ < Text style = { styles . title } > { t ( 'session.sharing.publicLink' ) } </ Text >
136+ < Item
137+ title = { t ( 'common.cancel' ) }
138+ onPress = { onCancel }
139+ />
140+ </ View >
96141
97- < ScrollView style = { styles . content } >
98- { ! publicShare || isConfiguring ? (
99- < ItemList >
142+ < ScrollView style = { styles . content } >
143+ { ! publicShare || isConfiguring ? (
144+ < ItemList >
100145 < Text style = { styles . description } >
101146 { t ( 'session.sharing.publicLinkDescription' ) }
102147 </ Text >
@@ -236,6 +281,23 @@ export const PublicLinkDialog = memo(function PublicLinkDialog({
236281 </ View >
237282 ) }
238283
284+ { /* Public link */ }
285+ { shareUrl ? (
286+ < >
287+ < Item
288+ title = { t ( 'session.sharing.publicLink' ) }
289+ subtitle = { < Text selectable > { shareUrl } </ Text > }
290+ subtitleLines = { 0 }
291+ onPress = { handleOpenLink }
292+ />
293+ < Item
294+ title = { t ( 'common.copy' ) }
295+ icon = { < Ionicons name = "copy-outline" size = { 29 } color = "#007AFF" /> }
296+ onPress = { handleCopyLink }
297+ />
298+ </ >
299+ ) : null }
300+
239301 { /* Info */ }
240302 { publicShare . token ? (
241303 < Item
@@ -288,8 +350,9 @@ export const PublicLinkDialog = memo(function PublicLinkDialog({
288350 </ View >
289351 </ ItemList >
290352 ) : null }
291- </ ScrollView >
292- </ View >
353+ </ ScrollView >
354+ </ View >
355+ </ BaseModal >
293356 ) ;
294357} ) ;
295358
0 commit comments