← back to Costa Rica App
costa-rica-app: big BEST WAY TO CONTACT block on listing — WhatsApp/Call/Email/Message/Other color buttons + in-app messenger composer modal
e1db3036fbea4bca5e4d30bcb50d6a57da4e65d1 · 2026-08-07 11:44:24 -0700 · Steve Abrams
Files touched
M app/listing/[slug].tsxM src/api.ts
Diff
commit e1db3036fbea4bca5e4d30bcb50d6a57da4e65d1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Aug 7 11:44:24 2026 -0700
costa-rica-app: big BEST WAY TO CONTACT block on listing — WhatsApp/Call/Email/Message/Other color buttons + in-app messenger composer modal
---
app/listing/[slug].tsx | 81 ++++++++++++++++++++++++++++++++++++++++++++++++--
src/api.ts | 4 +++
2 files changed, 82 insertions(+), 3 deletions(-)
diff --git a/app/listing/[slug].tsx b/app/listing/[slug].tsx
index ecd17b4..c793cf8 100644
--- a/app/listing/[slug].tsx
+++ b/app/listing/[slug].tsx
@@ -1,12 +1,25 @@
import { useEffect, useState } from 'react';
-import { View, Text, ScrollView, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native';
+import { View, Text, ScrollView, TouchableOpacity, StyleSheet, ActivityIndicator, Linking, Alert, TextInput, Modal } from 'react-native';
import { useLocalSearchParams, router } from 'expo-router';
-import { api, money } from '../../src/api';
+import { api, money, isAuthed } from '../../src/api';
export default function ListingDetail() {
const { slug } = useLocalSearchParams<{ slug: string }>();
const [l, setL] = useState<any>(null);
- useEffect(() => { api.listing(slug!).then(r => setL(r.listing)).catch(() => setL(false)); }, [slug]);
+ const [contact, setContact] = useState<any>(null);
+ const [msgOpen, setMsgOpen] = useState(false);
+ const [msg, setMsg] = useState('');
+ useEffect(() => {
+ api.listing(slug!).then(r => setL(r.listing)).catch(() => setL(false));
+ api.contact(slug!).then(r => setContact(r.contact)).catch(() => {});
+ }, [slug]);
+
+ async function sendMessage() {
+ if (!isAuthed()) { router.push('/login'); return; }
+ if (!msg.trim()) return;
+ try { await api.message(slug!, msg.trim()); setMsgOpen(false); setMsg(''); Alert.alert('Sent ✅', 'Your message was delivered. Replies show in your inbox.'); }
+ catch (e: any) { Alert.alert('Could not send', e.message); }
+ }
if (l === null) return <ActivityIndicator style={{ marginTop: 40 }} />;
if (l === false) return <Text style={s.err}>Not available.</Text>;
return (
@@ -22,7 +35,54 @@ export default function ListingDetail() {
<Text style={s.small}>Up to {l.max_guests} guests · {l.instant_book ? 'Instant book' : 'Request to book'} · {l.cancellation}</Text>
</View>
{l.address ? <Text style={s.small}>📍 {l.address}</Text> : null}
+
+ {/* BEST WAY TO CONTACT — big, prominent */}
+ <Text style={s.contactHead}>BEST WAY TO CONTACT</Text>
+ <View style={s.contactGrid}>
+ {contact?.whatsapp && (
+ <TouchableOpacity style={[s.cbtn, s.wa]} onPress={() => Linking.openURL(contact.whatsapp)}>
+ <Text style={s.cemoji}>💬</Text><Text style={s.cbtnTxt}>WhatsApp</Text>
+ </TouchableOpacity>
+ )}
+ {contact?.tel && (
+ <TouchableOpacity style={[s.cbtn, s.call]} onPress={() => Linking.openURL(contact.tel)}>
+ <Text style={s.cemoji}>📞</Text><Text style={s.cbtnTxt}>Call</Text>
+ </TouchableOpacity>
+ )}
+ {contact?.mailto && (
+ <TouchableOpacity style={[s.cbtn, s.email]} onPress={() => Linking.openURL(contact.mailto)}>
+ <Text style={s.cemoji}>✉️</Text><Text style={s.cbtnTxt}>Email</Text>
+ </TouchableOpacity>
+ )}
+ <TouchableOpacity style={[s.cbtn, s.msg]} onPress={() => (isAuthed() ? setMsgOpen(true) : router.push('/login'))}>
+ <Text style={s.cemoji}>📨</Text><Text style={s.cbtnTxt}>Message</Text>
+ </TouchableOpacity>
+ {contact?.website && (
+ <TouchableOpacity style={[s.cbtn, s.other]} onPress={() => Linking.openURL(contact.website)}>
+ <Text style={s.cemoji}>🌐</Text><Text style={s.cbtnTxt}>Other</Text>
+ </TouchableOpacity>
+ )}
+ </View>
</ScrollView>
+
+ {/* In-app messenger composer */}
+ <Modal visible={msgOpen} animationType="slide" transparent onRequestClose={() => setMsgOpen(false)}>
+ <View style={s.modalWrap}>
+ <View style={s.modalCard}>
+ <Text style={s.modalH}>Message {l.name}</Text>
+ <TextInput style={s.msgInput} placeholder="Ask about availability, price, anything…" value={msg}
+ onChangeText={setMsg} multiline autoFocus />
+ <View style={{ flexDirection: 'row', gap: 8, marginTop: 12 }}>
+ <TouchableOpacity style={[s.cta, { flex: 1, backgroundColor: '#eee', marginTop: 0 }]} onPress={() => setMsgOpen(false)}>
+ <Text style={[s.ctaTxt, { color: '#333' }]}>Cancel</Text>
+ </TouchableOpacity>
+ <TouchableOpacity style={[s.cta, { flex: 1, marginTop: 0 }]} onPress={sendMessage}>
+ <Text style={s.ctaTxt}>Send</Text>
+ </TouchableOpacity>
+ </View>
+ </View>
+ </View>
+ </Modal>
<TouchableOpacity style={s.cta} onPress={() => router.push(`/book/${slug}`)}>
<Text style={s.ctaTxt}>Book now</Text>
</TouchableOpacity>
@@ -42,4 +102,19 @@ const s = StyleSheet.create({
cta: { backgroundColor: '#0a7d55', margin: 16, height: 52, borderRadius: 12, alignItems: 'center', justifyContent: 'center' },
ctaTxt: { color: '#fff', fontSize: 17, fontWeight: '700' },
err: { textAlign: 'center', marginTop: 40, color: '#a33' },
+ // BEST WAY TO CONTACT
+ contactHead: { fontSize: 22, fontWeight: '900', letterSpacing: 1, color: '#1a2b22', marginTop: 26, marginBottom: 12, textAlign: 'center' },
+ contactGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 10, justifyContent: 'center' },
+ cbtn: { width: '47%', minHeight: 74, borderRadius: 14, alignItems: 'center', justifyContent: 'center', paddingVertical: 12 },
+ cemoji: { fontSize: 26 },
+ cbtnTxt: { color: '#fff', fontWeight: '800', fontSize: 16, marginTop: 4 },
+ wa: { backgroundColor: '#25D366' },
+ call: { backgroundColor: '#0a7d55' },
+ email: { backgroundColor: '#2b6cb0' },
+ msg: { backgroundColor: '#7a3cff' },
+ other: { backgroundColor: '#555' },
+ modalWrap: { flex: 1, backgroundColor: 'rgba(0,0,0,.4)', justifyContent: 'flex-end' },
+ modalCard: { backgroundColor: '#fff', borderTopLeftRadius: 20, borderTopRightRadius: 20, padding: 20 },
+ modalH: { fontSize: 18, fontWeight: '800', color: '#1a2b22', marginBottom: 12 },
+ msgInput: { minHeight: 90, borderWidth: 1, borderColor: '#e2e5e0', borderRadius: 12, padding: 12, textAlignVertical: 'top' },
});
diff --git a/src/api.ts b/src/api.ts
index 93225f6..1096df9 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -32,6 +32,10 @@ export const api = {
me: () => req('/me'),
listings: (q: Record<string, string> = {}) => req('/listings?' + new URLSearchParams(q).toString()),
listing: (slug: string) => req(`/listings/${slug}`),
+ contact: (slug: string) => req(`/listings/${slug}/contact`),
+ message: (slug: string, body: string) => req(`/listings/${slug}/message`, { method: 'POST', body: JSON.stringify({ body }) }),
+ threads: () => req('/threads'),
+ thread: (id: number) => req(`/threads/${id}`),
availability: (slug: string, from?: string, to?: string) =>
req(`/listings/${slug}/availability?` + new URLSearchParams({ ...(from && { from }), ...(to && { to }) } as any)),
createBooking: (b: any) => req('/bookings', { method: 'POST', body: JSON.stringify(b) }),
← 30a7d50 costa-rica-app: align react-native 0.76.9 + react-native-scr
·
back to Costa Rica App
·
costa-rica-app: add missing expo-asset + expo-font (Metro/EA a7eb8a4 →