React Native’de stepper form nasıl yapılır?
Anket sayfası gibi birçok girdi alan ekranlarda, basitçe ScrollView kullanıldığında, içerisinde sürekli aşağı yukarı gezilen bir formu doldurmayı kimse istemez. Bunun yerine stepper form’lar oldukça kullanıcı dostu bir deneyim sağlıyor. Bu yazımda da, “Stepper form nasıl kullanılır?” sorusuna bir alışveriş akışı formu üzerinden cevap vermeye çalışacağım.
Öncelikle projeminizi oluşturalım ve boş haliyle çalıştıralım:
npx react-native init SampleRNStepper --template react-native-template-typescript
cd SampleRNStepper
npx react-native run-ios
Projemize react-native-progress-steps
kütüphanesini ekleyelim, projeyi git’e ekleyelim ve vscode ile açalım
yarn add react-native-progress-steps
git init
git add .
git commit -m "First commit"
code .
react-native-progress-steps kütüphanesinin kullanımı
ProgressSteps kütüphanesinin en basit haliyle kullanımı aşağıdaki gibidir:
/* eslint-disable react-native/no-inline-styles */
import React from 'react';
import {SafeAreaView, StatusBar, Text} from 'react-native';
import {ProgressSteps, ProgressStep} from 'react-native-progress-steps';const App = () => {
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView style={{flex: 1}}>
<ProgressSteps>
<ProgressStep label="Sepetim">
<Text>Alışveriş Sepetiniz</Text>
</ProgressStep>
<ProgressStep label="Adres">
<Text>Adres bilgileri</Text>
</ProgressStep>
<ProgressStep label="Ödeme">
<Text>Ödeme bilgileri</Text>
</ProgressStep>
</ProgressSteps>
</SafeAreaView>
</>
);
};export default App;
Bu haliyle çok basit, ileri geri butonları İngilizce ve renkler de uyumsuz görünüyor öncelikle bir tema oluşturalım.
Temanın oluşturulması
Tema için 2 renk seçerek, ProgressSteps ve ProgressStep bileşenlerine ait olacak şekilde iki nesne oluşturarak özelliklerini ayarlayabiliriz. Tüm özelliklerin bulunduğu listeye buradan ulaşabilirsiniz. Ben aşağıdaki gibi örnek bir tema oluşturdum. Siz de themeColor değişkenini değiştirerek, mavi, yeşil vb. herhangi bir renkte tema oluşturabilirsiniz:
import React from 'react';
import {SafeAreaView, StatusBar, Text, StyleSheet} from 'react-native';
import {ProgressSteps, ProgressStep} from 'react-native-progress-steps';export const themeColor = '#1e1e1e';
export const textColor = '#ffffffdd';const App = () => {
const progressSteps = {
borderWidth: 3,
activeStepIconBorderColor: themeColor,
completedProgressBarColor: themeColor,
activeStepIconColor: themeColor,
activeLabelColor: themeColor,
completedStepNumColor: themeColor,
completedStepIconColor: themeColor,
activeStepNumColor: textColor,
};
const progressStep = {
nextBtnText: 'Sonraki >',
previousBtnText: '< Önceki',
finishBtnText: 'Gönder',
nextBtnStyle: styles.button,
previousBtnStyle: styles.button,
nextBtnTextStyle: styles.buttonText,
previousBtnTextStyle: styles.buttonText,
}; // İlk sayfada Önceki butonunun boş olarak görüntülenmemesi için gizliyoruz
const firstProgressStep = {
...progressStep,
previousBtnStyle: {
display: 'none',
},
}; return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView style={styles.safeAreaView}>
<ProgressSteps {...progressSteps}>
<ProgressStep label="Sepetim" {...firstProgressStep}>
<Text>Alışveriş Sepetiniz</Text>
</ProgressStep>
<ProgressStep label="Adres" {...progressStep}>
<Text>Adres bilgileri</Text>
</ProgressStep>
<ProgressStep label="Ödeme" {...progressStep}>
<Text>Ödeme bilgileri</Text>
</ProgressStep>
</ProgressSteps>
</SafeAreaView>
</>
);
};const styles = StyleSheet.create({
safeAreaView: {
flex: 1,
},
button: {
backgroundColor: themeColor,
paddingHorizontal: 16,
paddingVertical: 8,
},
buttonText: {
color: textColor,
fontSize: 16,
},
});export default App;
Uygulamayı çalıştırdığınızda ekran görüntüsü aşağıdaki gibi olacaktır:
Başlık kısmının stillendirilmesi
Sayfa içerisinde “Adres Bilgileri” yazan yerdeki metin için textHeader adında bir stil oluşturalım ve Text elemanlarına verelim.
...
<ProgressSteps>
<ProgressStep label="Sepetim" {...firstProgressStep}>
<Text style={styles.textHeader}>Alışveriş Sepetiniz</Text>
</ProgressStep>
<ProgressStep label="Adres" {...progressStep}>
<Text style={styles.textHeader}>Adres bilgileri</Text>
</ProgressStep>
<ProgressStep label="Ödeme" {...progressStep}>
<Text style={styles.textHeader}>Ödeme bilgileri</Text>
</ProgressStep>
</ProgressSteps>
...const styles = StyleSheet.create({
...
textHeader: {
fontSize: 36,
marginBottom: 24,
marginStart: 12,
marginTop: 0,
fontWeight: 'bold',
},
...
});
Ekran görüntüsü aşağıdaki gibi olacaktır:
Şimdi “Alışveriş sepeti” sayfasına geçebiliriz.
1. Alışveriş sepeti kodlanması
Proje içerisinde 3 adet sayfa için öncelikle bir src dizini oluşturalım. Ayrıca ikonların tutulacağı bir img dizini de oluşturalım. İkonları buradan indirebilirsiniz.
src dizininin içerisine 1_ShoppingCart.tsx isimli dosyayı aşağıdaki gibi oluşturalım:
/* eslint-disable react-native/no-inline-styles */
import React, {useState} from 'react';
import {
View,
FlatList,
StyleSheet,
Text,
Image,
TouchableOpacity,
Alert,
YellowBox,
} from 'react-native';const ShoppingCart = () => {
YellowBox.ignoreWarnings(['VirtualizedLists should never be nested']);const [data, setData] = useState([
{
uri: 'https://cdn.ikea.com.tr/urunler/190_190/PE770241.jpg',
title: 'MILLBERGET',
description: 'dönen sandalye, beyaz',
price: 269,
count: 1,
},
{
uri: 'https://cdn.ikea.com.tr/urunler/190_190/PE606741.jpg',
title: 'LANGFJALL',
description: 'kolçaklı dönen sandalye, gunnared mavi-siyah',
price: 1029,
count: 1,
},
{
uri: 'https://cdn.ikea.com.tr/urunler/190_190/PE343599.jpg',
title: 'LINNMON/ADILS',
description: 'çalışma masası, venge-siyah',
price: 1029,
count: 1,
},
{
uri: 'https://cdn.ikea.com.tr/urunler/190_190/PE673072.jpg',
title: 'NYMANE',
description: 'masa/duvar lambası, beyaz',
price: 1029,
count: 1,
},
]);const plusItem = (item: any) => {
setData(
data.map((x) =>
x.title === item.title ? {...x, count: item.count + 1} : x,
),
);
};const minusItem = (item: any) => {
if (item.count === 1) {
return;
}
setData(
data.map((x) =>
x.title === item.title ? {...x, count: item.count - 1} : x,
),
);
};const showRemoveItemDialog = (item) => {
Alert.alert(
'Uyarı',
'Ürünü sepetinizden çıkarmak istediğinize emin misiniz?',
[
{
text: 'Hayır',
onPress: () => console.log('Cancel Pressed'),
style: 'cancel',
},
{text: 'Evet', onPress: () => removeItem(item), style: 'destructive'},
],
{cancelable: true},
);
};const removeItem = (item) => {
setData(data.filter((x) => x.title !== item.title));
};return (
<FlatList
style={styles.list}
data={data}
keyExtractor={(item) => item.title}
ItemSeparatorComponent={() => <View style={styles.seperator} />}
renderItem={({item}) => (
<View style={{flexDirection: 'row'}}>
<Image source={{uri: item.uri}} style={styles.image} />
<View style={styles.itemTitleContainer}>
<View>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.description}>{item.description}</Text>
</View>
<View style={styles.itemBottomContainer}>
<Text style={styles.price}>
{(item.price * item.count).toLocaleString('tr')}₺
</Text>
<View style={styles.countContainer}>
<TouchableOpacity
style={styles.binButton}
onPress={() => showRemoveItemDialog(item)}>
<Image
source={require('../img/bin.png')}
style={styles.bin}
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.countButton}
onPress={() => minusItem(item)}>
<Text>-</Text>
</TouchableOpacity>
<Text style={styles.countText}>{item.count}</Text>
<TouchableOpacity
style={styles.countButton}
onPress={() => plusItem(item)}>
<Text>+</Text>
</TouchableOpacity>
</View>
</View>
</View>
</View>
)}
/>
);
};const styles = StyleSheet.create({
list: {
flex: 1,
paddingHorizontal: 16,
},
image: {
width: 100,
height: 100,
},
itemTitleContainer: {
justifyContent: 'space-between',
marginStart: 8,
flex: 1,
},
bin: {
width: 24,
height: 24,
},
binButton: {
marginEnd: 8,
},
itemBottomContainer: {
justifyContent: 'space-between',
flexDirection: 'row',
},
title: {
fontWeight: 'bold',
},
description: {
color: '#666',
maxWidth: 200,
},
price: {
fontSize: 24,
fontWeight: 'bold',
},
countContainer: {
flexDirection: 'row',
alignItems: 'center',
},
countButton: {
padding: 10,
backgroundColor: '#e5e5e5',
borderRadius: 3,
},
countText: {
marginHorizontal: 8,
},
seperator: {
marginVertical: 12,
height: StyleSheet.hairlineWidth,
backgroundColor: '#ddd',
},
});export default ShoppingCart;
Oluşturduğumuz bileşenin görüntülenmesi için App.tsx içerisinde “Alışveriş Sepetiniz” metninin altına ShoppingCart bileşenini ekleyelim:
<ProgressStep label="Sepetim" {...firstProgressStep}>
<Text style={styles.textHeader}>Alışveriş Sepetiniz</Text>
<ShoppingCart />
</ProgressStep>
Ekran görüntüsü aşağıdaki gibi olacaktır.
Kodu açıklamak gerekirse:
- YellowBox.ignoreWarnings: Varsayılan olarak react-native-progress-steps kütüphanesi bir ScrollView ile birlikte geliyor. İçerisine FlatList eklediğiniz takdirde sarı bir uyarı vererek “VirtualizedLists should never be nested” diyecektir. Normalde ScrollView içerisine FlatList eklememeniz gerekir ama şimdilik bunu kaldırmak için ignore edebiliriz.
- const [data, setData]: Bu kısımda görüntülenecek ürünleri state’te tutuyoruz.
- plusItem() ve minusItem(): Bu iki fonksiyonu, listede aynı ürüne ekleme veya çıkarma yapmak için kullanıyoruz.
- showRemoveItemDialog() ve removeItem(): Ürünü listeden silmek için bu iki fonksiyonu kullanıyoruz. showRemoveItemDialog ile ürünü sepetten çıkarmadan önce bir alertDialog görüntüleyerek kullanıcının yanlışlıkla ürünü silmesini engellemiş oluyoruz.
- {(item.price * item.count).toLocaleString(‘tr’)}₺: Ürün fiyatını noktalı şekilde görüntülemek için toLocaleString() fonksiyonunu kullanıyoruz.
Özetle aslında bir flatlist bileşeni ile tüm sayfayı tamamlamış bulunuyoruz. Şimdi Adres Bilgileri sayfasına geçelim
2. Adres Bilgileri sayfasının kodlanması
src dizini içerisine 2_Address.tsx dosyasını aşağıdaki gibi oluşturalım:
/* eslint-disable react-native/no-inline-styles */
import React, {useState} from 'react';
import {View, StyleSheet, Text, Image, TouchableOpacity} from 'react-native';const Address = () => {
const [data] = useState([
{
addressType: 'sending',
name: 'Zafer AYAN',
address: 'Mustafa Kemal Mh. Çiğdem Sk. No:3/45 34077, Esenler - İstanbul',
phoneNumber: '5327652345',
},
{
addressType: 'invoice',
name: 'Zafer AYAN',
address: 'Mustafa Kemal Mh. Çiğdem Sk. No:3/45 34077, Esenler - İstanbul',
phoneNumber: '5327652345',
invoice: 'individual',
tc: '11922384122',
},
]);
return (
<View style={styles.container}>
{data.map((item) => (
<View style={styles.card} key={item.addressType}>
<View style={styles.header}>
<Text style={styles.title}>
{item.addressType === 'sending' ? 'Gönderim' : 'Fatura'} Adresi
</Text>
<TouchableOpacity>
<Image
source={require('../img/edit.png')}
style={styles.editImage}
/>
</TouchableOpacity>
</View>
<View style={styles.seperator} />
<View style={styles.cardImageAndContent}>
<Image
source={
item.addressType === 'sending'
? require('../img/delivery.png')
: require('../img/bill.png')
}
style={styles.image}
/>
<View style={styles.addressColumn}>
<Text style={styles.name}>{item.name}</Text>
<Text style={styles.phoneNumber}>{item.phoneNumber}</Text>
<Text style={styles.address}>{item.address}</Text>
{item.addressType === 'invoice' && (
<View>
<Text style={styles.invoice}>
Fatura:{' '}
{item.invoice === 'individual' ? 'Bireysel' : 'Kurumsal'}
</Text>
<Text style={styles.tc}>
{item.invoice === 'individual' ? 'TC Kimlik No' : 'VKN No'}:{' '}
{item.tc}
</Text>
</View>
)}
</View>
</View>
</View>
))}
</View>
);
};const styles = StyleSheet.create({
container: {
paddingHorizontal: 16,
},
card: {
marginBottom: 24,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
},
editImage: {
width: 22,
height: 22,
},
cardImageAndContent: {
alignItems: 'center',
flexDirection: 'row',
},
image: {
width: 64,
height: 64,
},
addressColumn: {
flex: 1,
marginStart: 24,
},
title: {
fontSize: 18,
fontWeight: 'bold',
},
seperator: {
height: StyleSheet.hairlineWidth,
backgroundColor: '#ddd',
marginTop: 3,
marginBottom: 8,
},
name: {
fontSize: 18,
color: '#666',
marginBottom: 3,
},
address: {
fontSize: 18,
marginBottom: 3,
},
phoneNumber: {
fontSize: 18,
color: '#666',
marginBottom: 3,
},
invoice: {
fontSize: 18,
color: '#666',
marginBottom: 3,
},
tc: {
fontSize: 18,
color: '#666',
marginBottom: 3,
},
});export default Address;
Ayrıca App.tsx içerisinde “Adres bilgileri” metninin altına Address bileşenini ekleyelim:
<ProgressStep label="Adres" {...progressStep}>
<Text style={styles.textHeader}>Adres bilgileri</Text>
<Address />
</ProgressStep>
Ekran görüntüsü aşağıdaki gibi olacaktır.
Kodu açıklamak gerekirse:
- const [data]: Kısmında gönderim ve fatura adresi bilgilerini tuttum. Sadelik açısından adres bilgilerinin değiştirilmesi için bir arayüz eklemedim. Kalem ikonunu kodlayarak adres değiştirme için bir arayüz ekleyebilirsiniz.
- addressType: ‘sending’,: sending ve invoice adında iki adres tipi oluşturarak, adres tiplerine göre arayüzün özelleştirilmesini sağladım. Örneğin sadece fatura adresi kısmında TC kimlik no bilgisi yer alıyor.
Adres sayfası oldukça sade ve anlaşılır. Şimdi ödeme sayfasına geçelim.
3. Ödeme sayfasını kodlanması
src dizini içerisine 3_Payment.tsx dosyasını aşağıdaki gibi oluşturalım:
/* eslint-disable react-native/no-inline-styles */
import React, {useState, useRef} from 'react';
import {
View,
StyleSheet,
Text,
Image,
TouchableOpacity,
TextInput,
ActivityIndicator,
Keyboard,
} from 'react-native';interface Props {
handleSuccessScreen: () => void;
}const Payment = (props: Props) => {
const [card, setCard] = useState({
name: 'Özcan Zafer AYAN',
number: '',
expire: '',
cvv: '',
}); const [bill] = useState({
delivery: 109,
cargo: 0,
discount: 0,
expirationDifference: 0,
totalAmount: 109,
}); const [isLoading, setIsLoading] = useState(false); const refName = useRef<TextInput>(null);
const refNumber = useRef<TextInput>(null);
const refExpire = useRef<TextInput>(null);
const refCvv = useRef<TextInput>(null);
const refButton = useRef<TouchableOpacity>(null); var priceFormatter = new Intl.NumberFormat('tr-TR', {
style: 'currency',
currency: 'TRY',
}); const formatCardNumber = (value: string) => {
const regex = /^(\d{0,4})(\d{0,4})(\d{0,4})(\d{0,4})$/g;
const onlyNumbers = value.replace(/[^\d]/g, ''); return onlyNumbers.replace(regex, (regex, $1, $2, $3, $4) =>
[$1, $2, $3, $4].filter((group) => !!group).join(' '),
);
}; const formatExpire = (value: string) => {
const regex = /^(\d{0,2})(\d{0,2})$/g;
const onlyNumbers = value.replace(/[^\d]/g, ''); return onlyNumbers.replace(regex, (regex, $1, $2) =>
[$1, $2].filter((group) => !!group).join('/'),
);
}; const handleNumberChange = (text: string) => {
setCard({...card, number: text});
if (text.length === 19) {
refExpire.current!.focus();
} else if (text.length === 0) {
refName.current!.focus();
}
};
const handleExpireChange = (text: string) => {
setCard({...card, expire: text});
if (text.length === 5) {
refCvv.current!.focus();
} else if (text.length === 0) {
refNumber.current!.focus();
}
};
const handleCvvChange = (text: string) => {
setCard({...card, cvv: text});
if (text.length === 3) {
console.log('dismissed');
Keyboard.dismiss();
} else if (text.length === 0) {
refExpire.current!.focus();
}
};
const makePayment = () => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
props.handleSuccessScreen();
}, 2000);
};
return (
<View style={styles.container}>
<View style={styles.line}>
<Image source={require('../img/user.png')} style={styles.icon} />
<TextInput
style={styles.input}
placeholder="Kart üzerindeki isim"
keyboardType="name-phone-pad"
value={card.name}
ref={refName}
/>
</View>
<View style={styles.line}>
<Image source={require('../img/nfc.png')} style={styles.icon} />
<TextInput
style={styles.input}
placeholder="Kart numarası"
keyboardType="decimal-pad"
value={formatCardNumber(card.number)}
onChangeText={handleNumberChange}
maxLength={19}
ref={refNumber}
/>
</View>
<View style={styles.line}>
<Image source={require('../img/calendar.png')} style={styles.icon} />
<TextInput
style={styles.input}
placeholder="MM/YY"
keyboardType="decimal-pad"
value={formatExpire(card.expire)}
onChangeText={handleExpireChange}
ref={refExpire}
/>
<Image
source={require('../img/password.png')}
style={{...styles.icon, marginStart: 12, width: 46}}
/>
<TextInput
style={styles.input}
placeholder="CVV"
keyboardType="decimal-pad"
maxLength={3}
value={card.cvv}
onChangeText={handleCvvChange}
ref={refCvv}
onSubmit={() => Keyboard.dismiss()}
/>
</View><View style={styles.seperator} />
<View
style={{...styles.line, justifyContent: 'flex-end', marginBottom: 0}}>
<View style={{alignItems: 'flex-end'}}>
<Text style={styles.bill}>Sipariş Tutarı:</Text>
<Text style={styles.bill}>Kargo Tutarı:</Text>
<Text style={styles.bill}>İndirim Tutarı:</Text>
<Text style={styles.bill}>Vade Farkı:</Text>
</View>
<View style={{marginStart: 18, alignItems: 'flex-end'}}>
<Text style={styles.bill}>
{priceFormatter.format(bill.delivery)}
</Text>
<Text style={styles.bill}>{priceFormatter.format(bill.cargo)}</Text>
<Text style={styles.bill}>
{priceFormatter.format(bill.discount)}
</Text>
<Text style={styles.bill}>
{priceFormatter.format(bill.expirationDifference)}
</Text>
</View>
</View>
<View style={styles.seperator} />
<View style={{...styles.line, justifyContent: 'flex-end'}}>
<View style={{alignItems: 'flex-end'}}>
<Text style={styles.totalAmount}>Toplam:</Text>
</View>
<View style={{marginStart: 18, alignItems: 'flex-end'}}>
<Text style={styles.totalAmount}>
{priceFormatter.format(bill.totalAmount)}
</Text>
</View>
</View>
<View style={styles.line}>
<TouchableOpacity
style={styles.makePaymentButton}
onPress={makePayment}
disabled={isLoading}
ref={refButton}>
<ActivityIndicator
style={{
...styles.makePaymentLoading,
display: isLoading ? 'flex' : 'none',
}}
/>
<Text style={styles.makePaymentText}>
Ödeme {isLoading ? 'Yapılıyor' : 'Yap'}
</Text>
</TouchableOpacity>
</View>
</View>
);
};const styles = StyleSheet.create({
container: {
paddingHorizontal: 12,
flex: 1,
},
line: {
flexDirection: 'row',
flex: 1,
marginBottom: 24,
},
icon: {
width: 32,
height: 32,
marginEnd: 12,
},
input: {
borderColor: '#000',
borderBottomWidth: 2,
fontSize: 18,
flex: 1,
},
mmYyCvv: {
flexDirection: 'row',
},
bill: {
fontSize: 18,
marginBottom: 3,
},
totalAmount: {
fontSize: 24,
},
makePaymentButton: {
flex: 1,
backgroundColor: '#1e1e1e',
padding: 18,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
},
makePaymentText: {
color: '#e5e5e5',
textTransform: 'uppercase',
},
makePaymentLoading: {
marginEnd: 12,
},
seperator: {
height: StyleSheet.hairlineWidth,
backgroundColor: '#ddd',
marginTop: 3,
marginBottom: 8,
width: '60%',
alignSelf: 'flex-end',
},
});export default Payment;
Ayrıca App.tsx içerisinde “Ödeme bilgileri” metninin altına Payment bileşenini ekleyelim:
<ProgressStep label="Ödeme" {...lastProgressStep}>
<Text style={styles.textHeader}>Ödeme bilgileri</Text>
<Payment handleSuccessScreen={() => console.log('success')} />
</ProgressStep>
Ekran görüntüsü aşağıdaki gibi olacaktır:
Kodu açıklayacak olursak:
- interface Props { handleSuccessScreen: Ödeme işlemi tamamlandığında progress stepper ekranından çıkıp başarılı ekranına gitmek için bu prop type’ı tanımlıyoruz.
- const [card, setCard]: Kart bilgilerini bu değişkende state’te tutuyoruz.
- const [bill]: Sipariş tutarı gibi fatura kısımları için gereken bilgiyi bu değişkende tutuyoruz.
- const [isLoading, setIsLoading]: Ödeme yapılırken kullanıcı bekletildiği esnada loading ikonunun görüntülenmesi için bu bilgiyi state’te tutuyoruz.
- const refName, refNumber..: useRef hook’unu kullanarak TextInput bileşenlerini değişkenlerde tutuyoruz. Bu değişkenleri, kart numarası girildiğinde otomatik olarak sonraki TextInput’a focus’lanmak için kullanacağız.
- var priceFormatter = new Intl.NumberFormat: Fatura kısmındaki fiyat bilgisini formatlamak için kullanıyoruz.
- formatCardNumber(), formatExpire(): Kullanıcı kart numarasını girerken 4'lü şekilde formatlamak için formatCardNumber, son kullanma tarihinin formatlanması için de formatExpire metodunu kullanıyoruz.
- handleNumberChange(), handleExpireChange(), handleCvvChange(): Kredi kartı, son kullanma ve CVV kodu kısımlarında ilgili bilgi girildiğinde otomatik olarak sonraki input’a geçmek için kullanıyoruz. Bu metotlar sayesinde bilgiyi silerken de önceki input’a geçebiliyoruz.
- makePayment(): Ödeme yap butonuna tıklandığında loading ibaresinin görüntülenmesini ve 2 saniye sonra işlem başarılı ekranının açılmasını sağlıyoruz.
- onSubmit={() => Keyboard.dismiss()}: CVV kodu girildiğinde klavyenin gizlenmesini ve ödeme butonuna kolayca tıklanılmasını sağlıyoruz.
Input alanlarının validate edilmesi ile ilgili kısımları eklemedim. makePayment() metodunu değiştirerek kendiniz ekleyebilirsiniz.
Ödeme yap kısmından, başarılı ekranına gidilmesi için App.tsx’te isFinished değişkeni oluştururarak, stepper ekranı ile başarılı ekranını göster/gizle yapmayı sağlayacağız.
Başarılı ekranının kodlanması
isFinished değişkenini oluşturup state’e atayarak, arayüzü uygun şekilde değiştirdiğimizde App.tsx’in son hali aşağıdaki gibi olacaktır:
/* eslint-disable react-native/no-inline-styles */
import React, {useState} from 'react';
import {
SafeAreaView,
StyleSheet,
StatusBar,
View,
Text,
Image,
} from 'react-native';
import {ProgressSteps, ProgressStep} from 'react-native-progress-steps';
import ShoppingCart from './src/1_ShoppingCart';
import Address from './src/2_Address';
import Payment from './src/3_Payment';ShoppingCart;const App = () => {
const [isFinished, setIsFinished] = useState(false); const progressStep = {
nextBtnText: 'Sonraki >',
previousBtnText: '< Önceki',
nextBtnStyle: styles.button,
nextBtnTextStyle: styles.buttonText,
previousBtnStyle: styles.button,
previousBtnTextStyle: styles.buttonText,
};
const lastProgressStep = {
...progressStep,
nextBtnStyle: {
display: 'none',
},
};
const firstProgressStep = {
...progressStep,
previousBtnStyle: {
display: 'none',
},
};
const themeColor = '#1e1e1e';
const progressSteps = {
borderWidth: 3,
activeStepIconBorderColor: themeColor,
completedProgressBarColor: themeColor,
activeStepIconColor: themeColor,
activeLabelColor: themeColor,
completedStepNumColor: themeColor,
completedStepIconColor: themeColor,
activeStepNumColor: '#e5e5e5',
}; return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView style={styles.safeAreaView}>
<View style={styles.container}>
{!isFinished && (
<ProgressSteps {...progressSteps}>
<ProgressStep label="Sepetim" {...firstProgressStep}>
<Text style={styles.textHeader}>Alışveriş Sepetiniz</Text>
<ShoppingCart />
</ProgressStep>
<ProgressStep label="Adres" {...progressStep}>
<Text style={styles.textHeader}>Adres bilgileri</Text>
<Address />
</ProgressStep>
<ProgressStep label="Ödeme" {...lastProgressStep}>
<Text style={styles.textHeader}>Ödeme bilgileri</Text>
<Payment handleSuccessScreen={() => setIsFinished(true)} />
</ProgressStep>
</ProgressSteps>
)}
{isFinished && (
<View style={styles.success}>
<Text style={styles.successText}>
Ödeme başarıyla gerçekleşti
</Text>
<Image style={styles.successImg} source={require('./img/tick.png')} />
</View>
)}
</View>
</SafeAreaView>
</>
);
};const styles = StyleSheet.create({
safeAreaView: {
flex: 1,
},
container: {
flex: 1,
padding: 10,
},
textHeader: {
fontSize: 36,
marginBottom: 24,
marginStart: 12,
marginTop: 0,
fontWeight: 'bold',
},
button: {
backgroundColor: '#1e1e1e',
paddingHorizontal: 16,
paddingVertical: 8,
},
buttonText: {
color: '#e5e5e5',
fontSize: 16,
},
successText: {
fontSize: 36,
fontWeight: 'bold',
textAlign: 'center',
},
success: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
successImg: {
width: 72,
height: 72,
marginTop: 24,
},
});export default App;
Ödeme yap butonuna basılıp yeni ekrana geçildiğinde ekran görüntüsü aşağıdaki gibi olacaktır:
Sonuç olarak
Stepper form’lar, kullanıcıdan birçok bilginin alınması için iyi bir UX sunar. Eğer uygulamanızda çok parametre alan bir API’ye istek yapıyorsanız stepper form kullanmanız en iyi çözüm olacaktır. react-native-progress-steps
kütüphanesi bu noktada oldukça iyi iş görüyor. Kütüphanenin ihtiyaçlarınızı karşılamadığı durumlarda kendiniz de React Native Navigation kütüphanesini kullanarak native bir stepper form oluşturabilirsiniz.
Projenin bitmiş halini react-native-progress-steps-sample reposunda bulabilirsiniz. Bu yazı hakkında soru ve görüşlerinizi aşağıdaki yorumlar kısmından yazabilirsiniz. Bana destek vermek için alkış simgesine tıklayabilirsiniz. Sonraki yazımda görüşmek üzere…