feat: render CNAME and Cloudflare SSL TXT verification instructions with copy buttons in mobile app domain settings
This commit is contained in:
@@ -30,6 +30,24 @@ interface RestaurantDetail {
|
|||||||
address: string | null;
|
address: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TxtRecord {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DomainInstructions {
|
||||||
|
cname?: { type: string; host: string; target: string };
|
||||||
|
ownershipTxt?: TxtRecord | null;
|
||||||
|
sslTxt?: TxtRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CloudflareStatus {
|
||||||
|
hostnameStatus?: string;
|
||||||
|
sslStatus?: string;
|
||||||
|
ownershipTxt?: TxtRecord | null;
|
||||||
|
sslTxt?: TxtRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
interface DomainRow {
|
interface DomainRow {
|
||||||
id: string;
|
id: string;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
@@ -37,6 +55,8 @@ interface DomainRow {
|
|||||||
status: "pending" | "verified" | "failed";
|
status: "pending" | "verified" | "failed";
|
||||||
verified_at: string | null;
|
verified_at: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
instructions?: DomainInstructions;
|
||||||
|
cloudflareStatus?: CloudflareStatus | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const THEME_OPTIONS = [
|
const THEME_OPTIONS = [
|
||||||
@@ -192,15 +212,20 @@ export default function AccountScreen() {
|
|||||||
setAddingDomain(true);
|
setAddingDomain(true);
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
|
||||||
try {
|
try {
|
||||||
const res = await api.post<{ domain: DomainRow }>(`/restaurants/${active.restaurantId}/domains`, {
|
const res = await api.post<{ domain: DomainRow; instructions?: DomainInstructions; cnameTarget?: string }>(
|
||||||
hostname: newDomainInput.trim(),
|
`/restaurants/${active.restaurantId}/domains`,
|
||||||
});
|
{ hostname: newDomainInput.trim() },
|
||||||
setDomains((prev) => [res.domain, ...prev]);
|
);
|
||||||
|
const updatedDomain: DomainRow = {
|
||||||
|
...res.domain,
|
||||||
|
instructions: res.instructions,
|
||||||
|
};
|
||||||
|
setDomains((prev) => [updatedDomain, ...prev]);
|
||||||
setNewDomainInput("");
|
setNewDomainInput("");
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"Alan Adı Eklendi 🎉",
|
"Alan Adı Eklendi 🎉",
|
||||||
`Lütfen DNS sağlayıcınızda (GoDaddy, Cloudflare vb.) ${res.domain.hostname} için CNAME kaydını ${cnameTarget} adresine yönlendirin.`,
|
`Lütfen DNS sağlayıcınızda (GoDaddy, Cloudflare vb.) yönlendirme (CNAME) ve varsa SSL TXT doğrulama kayıtlarını ekleyin.`,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Alert.alert("Hata", err instanceof Error ? err.message : "Alan adı eklenemedi.");
|
Alert.alert("Hata", err instanceof Error ? err.message : "Alan adı eklenemedi.");
|
||||||
@@ -214,15 +239,19 @@ export default function AccountScreen() {
|
|||||||
setVerifyingDomainId(domainId);
|
setVerifyingDomainId(domainId);
|
||||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
|
||||||
try {
|
try {
|
||||||
const res = await api.post<{ verified: boolean; domain: DomainRow; message: string }>(
|
const res = await api.post<{ verified: boolean; domain: DomainRow; cloudflareStatus?: CloudflareStatus; message: string }>(
|
||||||
`/restaurants/${active.restaurantId}/domains/${domainId}/verify`,
|
`/restaurants/${active.restaurantId}/domains/${domainId}/verify`,
|
||||||
);
|
);
|
||||||
|
const updatedDomain: DomainRow = {
|
||||||
|
...res.domain,
|
||||||
|
cloudflareStatus: res.cloudflareStatus,
|
||||||
|
};
|
||||||
if (res.verified) {
|
if (res.verified) {
|
||||||
setDomains((prev) => prev.map((d) => (d.id === domainId ? res.domain : d)));
|
setDomains((prev) => prev.map((d) => (d.id === domainId ? updatedDomain : d)));
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
|
||||||
Alert.alert("Tebrikler! 🎉", res.message);
|
Alert.alert("Tebrikler! 🎉", res.message);
|
||||||
} else {
|
} else {
|
||||||
setDomains((prev) => prev.map((d) => (d.id === domainId ? res.domain : d)));
|
setDomains((prev) => prev.map((d) => (d.id === domainId ? updatedDomain : d)));
|
||||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning).catch(() => {});
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning).catch(() => {});
|
||||||
Alert.alert("Doğrulanamadı", res.message);
|
Alert.alert("Doğrulanamadı", res.message);
|
||||||
}
|
}
|
||||||
@@ -673,25 +702,135 @@ export default function AccountScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* CNAME instructions if pending */}
|
{/* DNS & SSL Instructions if Pending */}
|
||||||
{!isVerified ? (
|
{!isVerified ? (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
marginTop: 10,
|
marginTop: 12,
|
||||||
padding: 10,
|
padding: 12,
|
||||||
backgroundColor: "#FFFFFF",
|
backgroundColor: "#FFFFFF",
|
||||||
|
borderRadius: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#E7E5E4",
|
||||||
|
gap: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 12, fontWeight: "800", color: "#1C1917" }}>
|
||||||
|
📌 DNS & SSL Doğrulama Talimatları
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: 11, color: "#78716C", lineHeight: 16 }}>
|
||||||
|
Domain sağlayıcınızın (GoDaddy, Natro, Cloudflare vb.) DNS paneline aşağıdaki kayıtları ekleyin:
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* 1. CNAME Record */}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#FAF8F5",
|
||||||
|
padding: 10,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: "#E7E5E4",
|
borderColor: "#E7E5E4",
|
||||||
|
gap: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ fontSize: 11, fontWeight: "700", color: "#44403C" }}>
|
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
DNS Kaydı Talimatı:
|
<Text style={{ fontSize: 11, fontWeight: "800", color: "#C8A96B" }}>
|
||||||
|
1. CNAME Yönlendirme Kaydı (Zorunlu)
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: 11, color: "#78716C", marginTop: 2 }}>
|
<Pressable
|
||||||
Tür: <Text style={{ fontWeight: "700", color: "#1C1917" }}>CNAME</Text> | Hedef:{" "}
|
onPress={() => {
|
||||||
<Text style={{ fontWeight: "700", color: "#C8A96B" }}>{cnameTarget}</Text>
|
Clipboard.setStringAsync(cnameTarget);
|
||||||
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
|
||||||
|
Alert.alert("Kopyalandı! 📋", `CNAME Hedefi (${cnameTarget}) panoya kopyalandı.`);
|
||||||
|
}}
|
||||||
|
style={{ backgroundColor: "#F59E0B20", paddingHorizontal: 8, paddingVertical: 2, borderRadius: 6 }}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 10, fontWeight: "700", color: "#D97706" }}>Kopyala</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
<Text style={{ fontSize: 11, color: "#57534E" }}>
|
||||||
|
Tür: <Text style={{ fontWeight: "700", color: "#1C1917" }}>CNAME</Text>
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text style={{ fontSize: 11, color: "#57534E" }}>
|
||||||
|
Host: <Text style={{ fontWeight: "700", color: "#1C1917" }}>{dom.hostname}</Text>
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontSize: 11, color: "#57534E" }}>
|
||||||
|
Hedef: <Text style={{ fontWeight: "700", color: "#C8A96B" }}>{cnameTarget}</Text>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 2. TXT Verification Records (If returned by Cloudflare for SSL) */}
|
||||||
|
{(() => {
|
||||||
|
const txts: TxtRecord[] = [];
|
||||||
|
if (dom.instructions?.ownershipTxt) txts.push(dom.instructions.ownershipTxt);
|
||||||
|
if (dom.instructions?.sslTxt) txts.push(...dom.instructions.sslTxt);
|
||||||
|
if (dom.cloudflareStatus?.ownershipTxt) txts.push(dom.cloudflareStatus.ownershipTxt);
|
||||||
|
if (dom.cloudflareStatus?.sslTxt) txts.push(...dom.cloudflareStatus.sslTxt);
|
||||||
|
|
||||||
|
const uniqueTxts = txts.filter(
|
||||||
|
(txt, index, self) =>
|
||||||
|
index === self.findIndex((t) => t.name === txt.name && t.value === txt.value),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (uniqueTxts.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ gap: 8 }}>
|
||||||
|
<Text style={{ fontSize: 11, fontWeight: "800", color: "#0284C7", marginTop: 2 }}>
|
||||||
|
2. SSL Sertifikası TXT Doğrulama Kaydı (Gerekliyse):
|
||||||
|
</Text>
|
||||||
|
{uniqueTxts.map((txt, idx) => (
|
||||||
|
<View
|
||||||
|
key={idx}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#F0F9FF",
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: "#BAE6FD",
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 10, fontWeight: "800", color: "#0369A1" }}>
|
||||||
|
TXT Kaydı #{idx + 1} (SSL Doğrulama)
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
|
<Text style={{ fontSize: 11, color: "#334155", flex: 1, marginRight: 6 }}>
|
||||||
|
İsim (Name): <Text style={{ fontWeight: "700", color: "#0F172A" }}>{txt.name}</Text>
|
||||||
|
</Text>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
Clipboard.setStringAsync(txt.name);
|
||||||
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
|
||||||
|
Alert.alert("Kopyalandı! 📋", `TXT İsmi (${txt.name}) panoya kopyalandı.`);
|
||||||
|
}}
|
||||||
|
style={{ backgroundColor: "#0284C720", paddingHorizontal: 6, paddingVertical: 3, borderRadius: 4 }}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 10, fontWeight: "700", color: "#0284C7" }}>İsim Kopyala</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "center" }}>
|
||||||
|
<Text numberOfLines={1} style={{ fontSize: 11, color: "#334155", flex: 1, marginRight: 6 }}>
|
||||||
|
Değer (Value): <Text style={{ fontWeight: "700", color: "#0F172A" }}>{txt.value}</Text>
|
||||||
|
</Text>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
Clipboard.setStringAsync(txt.value);
|
||||||
|
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
|
||||||
|
Alert.alert("Kopyalandı! 📋", `TXT Değeri panoya kopyalandı.`);
|
||||||
|
}}
|
||||||
|
style={{ backgroundColor: "#0284C720", paddingHorizontal: 6, paddingVertical: 3, borderRadius: 4 }}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 10, fontWeight: "700", color: "#0284C7" }}>Değer Kopyala</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
Reference in New Issue
Block a user