Apa yang Saya Pelajari Membuat Aplikasi Game Kata Dengan Nuxt di Google Play

Saya jatuh cinta dengan pengkodean saat saya membuat CSS pertama :hover efek hover. Bertahun-tahun kemudian, gigitan awal ke dalam interaktivitas di web membawa saya ke tujuan baru: membuat game.


Saat-saat awal bermain dengan :hover bukanlah hal yang istimewa, atau bahkan berguna. Saya ingat membuat grid responsif dari kotak biru (dibuat dengan float , jika itu memberi Anda gambaran tentang timeline), yang masing-masing berubah menjadi oranye saat kursor digerakkan di atasnya. Saya menghabiskan waktu berjam-jam mengamati kotak-kotak itu, mengubah ukuran jendela untuk melihat mereka mengubah ukuran dan kesejajaran, lalu melakukannya lagi. Rasanya seperti sihir murni.

Apa yang saya buat di web secara alami menjadi lebih kompleks daripada kisi <div> selama bertahun-tahun, tetapi sensasi menghadirkan sesuatu yang benar-benar interaktif ke kehidupan selalu melekat pada saya. Dan saat saya belajar lebih banyak tentang JavaScript, saya sangat suka membuat game.

Terkadang itu hanya demo CodePen; terkadang itu adalah proyek sampingan kecil yang diterapkan di Vercel atau Netlify. Saya menyukai tantangan membuat ulang game seperti color flood , hangman , atau Connect Four di browser.

Namun, setelah beberapa saat, tujuan menjadi lebih besar: bagaimana jika saya membuat game yang sebenarnya? Bukan hanya aplikasi web; permainan nyata, jujur-untuk-kebaikan, unduh-dari-toko-aplikasi. Agustus lalu, saya mulai mengerjakan proyek saya yang paling ambisius hingga saat ini, dan empat bulan kemudian, saya merilisnya ke dunia ( baca: bosan mengutak-atiknya ): aplikasi permainan kata yang saya sebut Quina .

Apa permainannya (dan apa nama itu)?

Cara termudah untuk menjelaskan Quina adalah: ini Mastermind , tapi dengan kata lima huruf. Nyatanya, Mastermind sebenarnya adalah versi dari permainan pena dan kertas klasik; Quina hanyalah variasi lain dari game orisinal yang sama.

Tujuan Quina adalah menebak kata lima huruf rahasia. Setelah setiap tebakan, Anda mendapatkan petunjuk yang memberi tahu Anda seberapa dekat tebakan Anda dengan kata kode. Anda menggunakan petunjuk itu untuk memperbaiki tebakan Anda berikutnya, dan seterusnya, tetapi Anda hanya mendapatkan total sepuluh tebakan; habis dan kamu kalah.

Contoh gameplay Quina

Nama "Quina" muncul karena artinya "lima sekaligus" dalam bahasa Latin (atau begitulah yang dikatakan Google kepada saya). Permainan tradisional biasanya dimainkan dengan kata-kata empat huruf, atau kadang-kadang empat digit (atau dalam kasus Mastermind, empat warna); Quina menggunakan kata-kata yang terdiri dari lima huruf tanpa mengulang huruf, jadi rasanya cocok jika game tersebut memiliki nama yang sesuai dengan aturannya sendiri. (Saya tidak tahu bagaimana kata Latin asli diucapkan, tapi saya mengatakannya "QUINN-ah," yang mungkin salah, tapi hei, ini permainan saya, bukan?)

Saya menghabiskan malam hari dan akhir pekan selama sekitar empat bulan membangun aplikasi. Saya ingin menghabiskan artikel ini berbicara tentang teknologi di balik permainan, keputusan yang terlibat, dan pelajaran yang dipetik jika ini adalah jalan yang ingin Anda jalani sendiri.

Memilih Nuxt

Saya penggemar berat Vue , dan ingin menggunakan proyek ini sebagai cara untuk memperluas pengetahuan saya tentang ekosistemnya. Saya mempertimbangkan untuk menggunakan kerangka kerja lain (saya juga membangun proyek di Svelte dan React), tetapi saya merasa Nuxt mencapai titik manis dari keakraban, kemudahan penggunaan, dan kedewasaan. (Ngomong-ngomong, jika Anda tidak tahu atau tidak menebak: Nuxt bisa secara adil digambarkan sebagai Vue yang setara dengan Next.js. )

Saya tidak terlalu mendalam dengan Nuxt sebelumnya; hanya beberapa aplikasi yang sangat kecil. Tapi saya tahu Nuxt dapat mengkompilasi ke aplikasi statis , yang saya inginkan – tidak ada server (Node) yang perlu dikhawatirkan. Saya juga tahu Nuxt dapat menangani perutean semudah menjatuhkan komponen Vue ke /pages , yang sangat menarik.

Plus, meskipun Vuex (manajemen resmi negara di Vue) sendiri tidak terlalu rumit, saya menghargai cara Nuxt menambahkan sedikit gula untuk membuatnya lebih sederhana. (Nuxt mempermudah hal-hal dalam berbagai cara, seperti tidak mengharuskan Anda untuk mengimpor komponen secara eksplisit sebelum Anda dapat menggunakannya; Anda cukup meletakkannya di markup dan Nuxt akan mengetahuinya dan mengimpor otomatis sebagai dibutuhkan.)

Akhirnya, saya tahu sebelumnya saya sedang membangun Aplikasi Web Progresif (PWA), jadi fakta bahwa sudah ada modul Nuxt PWA untuk membantu membangun semua fitur yang terlibat (seperti pekerja layanan untuk kemampuan offline) yang sudah dikemas dan siap untuk pergi adalah hasil imbang besar. Faktanya, ada banyak sekali modul Nuxt yang tersedia untuk semua rintangan yang tidak terlihat. Itu menjadikan Nuxt pilihan termudah, paling jelas, dan yang tidak pernah saya sesali.

Saya akhirnya menggunakan lebih banyak modul saat saya pergi, termasuk modul Konten Nuxt bintang, yang memungkinkan Anda untuk menulis konten halaman dalam penurunan harga, atau bahkan campuran komponen penurunan harga dan Vue. Saya menggunakan fitur yang untuk “Tanya Jawab” halaman dan “Cara Bermain” halaman juga (karena menulis dalam penurunan harga adalah jauh lebih bagus daripada halaman HTML hard-coding).

Mencapai nuansa aplikasi asli dengan web

Quina akhirnya akan menemukan rumah di Google Play Store , tetapi terlepas dari bagaimana atau di mana itu dimainkan, saya ingin itu terasa seperti aplikasi yang lengkap sejak awal.

Untuk memulai, itu berarti mode gelap opsional, dan pengaturan untuk mengurangi gerakan untuk kegunaan optimal, seperti yang dimiliki banyak aplikasi asli (dan dalam kasus gerakan yang dikurangi, seperti yang seharusnya dimiliki oleh apa pun dengan animasi).

Di bawah tenda, kedua pengaturan pada akhirnya adalah boolean di penyimpanan data Vuex aplikasi. Jika true , setelan akan merender kelas tertentu dalam tata letak default aplikasi. Layout Nuxt adalah template Vue yang "membungkus" semua konten Anda, dan merender di semua (atau banyak) halaman aplikasi Anda (biasanya digunakan untuk hal-hal seperti header dan footer bersama, tetapi juga berguna untuk pengaturan global):

 <!-- layouts/default.vue --> <template> <div :class="[ { 'dark-mode': darkMode, 'reduce-motion': reduceMotion, }, 'dots', ]" > <Nuxt /> </div> </template> <script> import { mapGetters } from 'vuex' export default { computed: { ...mapGetters(['darkMode', 'reduceMotion']), }, // Other layout component code here } </script>

Berbicara tentang pengaturan: meskipun aplikasi web dibagi menjadi beberapa halaman berbeda – menu, pengaturan, tentang, putar, dll. – penyimpanan data Vuex global bersama membantu menjaga hal-hal tetap sinkron dan terasa mulus antar area aplikasi (karena pengguna akan menyesuaikan pengaturannya di satu halaman, dan melihatnya berlaku untuk game di halaman lain).

Setiap pengaturan di aplikasi juga disinkronkan ke localStorage dan Vuex store, yang memungkinkan menyimpan dan memuat nilai antar sesi, selain melacak pengaturan saat pengguna menavigasi antar halaman.

Dan berbicara tentang navigasi: berpindah antar halaman adalah area lain di mana saya merasa ada banyak peluang untuk membuat Quina terasa seperti aplikasi asli, dengan menambahkan transisi halaman penuh.

Transisi halaman Nuxt beraksi

Transisi Vue secara umum cukup mudah — Anda cukup menulis kelas CSS yang diberi nama khusus untuk status transisi "ke" dan "dari" Anda — tetapi Nuxt melangkah lebih jauh dan memungkinkan Anda untuk menyetel transisi halaman penuh hanya dengan satu baris di halaman File Vue:

 <!-- A page component, eg, pages/Options.vue --> <script> export default { transition: 'page-slide' // ... The rest of the component properties } </script>

transition sangat kuat; ini memberi tahu Nuxt bahwa kita ingin page-slide diterapkan ke halaman ini setiap kali kita menavigasi ke atau keluar darinya. Dari sana, yang perlu kita lakukan adalah menentukan kelas yang menangani animasi, seperti yang Anda lakukan dengan transisi Vue. Ini SCSS page-slide

 /* assets/css/_animations.scss */ .page-slide { &-enter-active { transition: all 0.35s cubic-bezier(0, 0.25, 0, 0.75); } &-leave-active { transition: all 0.35s cubic-bezier(0.75, 0, 1, 0.75); } &-enter, &-leave-to { opacity: 0; transform: translateY(1rem); .reduce-motion & { transform: none !important; } } &-leave-to { transform: translateY(-1rem); } }

Perhatikan kelas .reduce-motion ; itulah yang kita bicarakan di file tata letak tepat di atas. Ini mencegah gerakan visual ketika pengguna telah menunjukkan bahwa mereka lebih suka gerakan yang dikurangi (baik melalui kueri media atau pengaturan manual), dengan menonaktifkan transform apa pun (yang tampaknya menjamin penggunaan !important memecah belah). Opacity masih dibiarkan memudar masuk dan keluar, karena ini sebenarnya bukan gerakan.

Membandingkan gerakan default (kiri) dengan gerakan yang dikurangi (kanan)

Catatan tambahan tentang transisi dan penanganan 404: Transisi dan perutean, tentu saja, ditangani oleh JavaScript di bawah tenda (Vue Router, tepatnya), tetapi saya mengalami masalah yang membuat frustrasi di mana skrip akan berhenti berjalan di halaman idle (misalnya , jika pengguna meninggalkan aplikasi atau tab terbuka di latar belakang untuk sementara waktu). Ketika kembali ke halaman idle tersebut dan mengklik link, Vue Router akan berhenti berjalan, dan link tersebut akan diperlakukan sebagai relatif dan 404.

Contoh: halaman /faq tidak aktif; pengguna kembali ke sana dan mengklik tautan untuk mengunjungi halaman /options . Aplikasi akan mencoba masuk ke /faq/options , yang tentu saja tidak ada.

Solusi saya untuk ini adalah error.vue khusus (ini adalah halaman Nuxt yang secara otomatis menangani semua kesalahan), di mana saya akan menjalankan validasi pada jalur masuk dan mengarahkan ke ujung jalur.

 // layouts/error.vue mounted() { const lastPage = '/' + this.$route.fullPath.split('/').pop() // Don't create a redirect loop if (lastPage !== this.$route.fullPath) { this.$router.push({ path: lastPage, }) } }

Ini berfungsi untuk kasus penggunaan saya karena a) Saya tidak memiliki rute bersarang; dan b) pada akhirnya, jika jalurnya tidak valid, masih mencapai 404.

Getaran dan suara

Transisi memang bagus, tetapi saya juga tahu Quina tidak akan terasa seperti aplikasi asli – terutama di ponsel cerdas – tanpa getaran dan suara.

Getaran relatif mudah dicapai di browser saat ini, berkat API Navigator . Sebagian besar browser modern hanya memungkinkan Anda memanggil window.navigator.vibrate() untuk memberi pengguna sedikit buzz atau serangkaian buzz – atau, menggunakan durasi yang sangat singkat, sedikit umpan balik taktil, seperti saat Anda mengetuk tombol pada keyboard smartphone.

Jelas, Anda ingin menggunakan getaran dengan hemat, karena beberapa alasan. Pertama, karena terlalu banyak dapat dengan mudah menjadi pengalaman pengguna yang buruk; dan kedua, karena tidak semua perangkat / browser mendukungnya, jadi Anda harus sangat berhati-hati tentang bagaimana dan di mana Anda mencoba memanggil fungsi vibrate() , jangan sampai Anda menyebabkan kesalahan yang mematikan skrip yang sedang berjalan.

Secara pribadi, solusi saya adalah mengatur pengambil Vuex untuk memverifikasi bahwa pengguna mengizinkan getaran (ini dapat dinonaktifkan dari halaman pengaturan); bahwa konteks saat ini adalah klien, bukan server; dan terakhir, fungsi tersebut ada di browser saat ini. ( Rantai opsional ES2020 akan berfungsi di sini juga untuk bagian terakhir itu.)

 // store/getters.js vibration(state) { if ( process.client && state.options.vibration && typeof window.navigator.vibrate !== 'undefined' ) { return true } return false },

Catatan tambahan: Memeriksa process.client penting di Nuxt – dan banyak kerangka kerja lain dengan kode yang dapat berjalan di Node – karena window tidak selalu ada. Ini berlaku bahkan jika Anda menggunakan Nuxt dalam mode statis, karena komponen divalidasi di Node selama waktu pembuatan. process.client (dan kebalikannya, process.server ) adalah kebaikan Nuxt yang hanya memvalidasi lingkungan kode saat ini pada waktu proses, jadi sempurna untuk mengisolasi kode khusus browser.

Suara adalah bagian penting lain dari pengalaman pengguna aplikasi. Alih-alih membuat efek saya sendiri (yang pasti akan menambahkan puluhan jam lagi ke proyek), saya mencampur sampel dari beberapa seniman yang lebih tahu apa yang mereka lakukan di dunia itu, dan yang menawarkan beberapa suara game gratis secara online. (Lihat FAQ aplikasi untuk info lengkap.)

Pengguna dapat mengatur volume yang mereka sukai, atau mematikan suara sepenuhnya. Ini, dan getaran, juga disetel di localStorage di browser pengguna serta disinkronkan ke toko Vuex. Pendekatan ini memungkinkan kita menyetel setelan "permanen" yang disimpan di browser, tetapi tanpa perlu mengambilnya dari browser setiap kali direferensikan. (Suara, misalnya, periksa level volume saat ini setiap kali dimainkan, dan latensi menunggu localStorage setiap kali itu terjadi bisa cukup untuk mematikan pengalaman.)

Kesampingkan suara

Ternyata untuk alasan apa pun, Safari sangat lamban dalam hal suara. Semua klik, boops, dan bantingan akan memakan waktu yang cukup lama setelah peristiwa yang memicu mereka untuk benar-benar bermain di Safari, terutama di iOS. Itu adalah pemecah kesepakatan, dan lubang kelinci yang saya habiskan berjam-jam dengan putus asa untuk menggali terowongan.

Untungnya, saya menemukan perpustakaan bernama Howler.js yang memecahkan masalah suara lintas platform dengan cukup mudah (dan itu juga memiliki logo kecil yang menyenangkan). Cukup menginstal Howler sebagai dependensi dan menjalankan semua suara aplikasi melaluinya – pada dasarnya satu atau dua baris kode – sudah cukup untuk menyelesaikan masalah.

Logo Howler.js, menampilkan seekor monyet howler kecil yang lucu mengenakan headphone

Jika Anda membuat aplikasi JavaScript dengan suara sinkron, saya sangat merekomendasikan penggunaan Howler, karena saya tidak tahu apa masalah Safari atau bagaimana Howler menyelesaikannya. Tidak ada yang berhasil saya coba, jadi saya senang masalah ini diselesaikan dengan mudah dengan sedikit biaya tambahan atau modifikasi kode.

Gameplay, sejarah, dan penghargaan

Quina bisa menjadi permainan yang sulit, terutama pada awalnya, jadi ada beberapa cara untuk menyesuaikan tingkat kesulitan permainan agar sesuai dengan preferensi pribadi Anda:

  1. Anda dapat memilih jenis kata yang ingin Anda dapatkan sebagai kata kode: Basic (kata umum dalam bahasa Inggris), Tricky (kata yang lebih tidak jelas atau lebih sulit dieja), atau Random (campuran berbobot keduanya).
  2. Anda dapat memilih apakah akan menerima petunjuk di awal setiap permainan, dan jika demikian, seberapa banyak petunjuk itu terungkap.
Quina menawarkan beberapa cara bermain yang berbeda, untuk mengakomodasi pemain dari semua tingkat keahlian

Pengaturan ini memungkinkan pemain dari berbagai keahlian, usia, dan / atau kemahiran bahasa Inggris untuk memainkan game di level mereka sendiri. (Kumpulan kata dasar dengan petunjuk yang kuat akan menjadi yang paling mudah; Rumit atau Acak tanpa petunjuk akan menjadi yang paling sulit.)

"Petunjuk halus" mengungkapkan satu huruf dalam kata kode (tetapi bukan posisinya)

Meskipun hanya memainkan serangkaian game satu kali dengan kesulitan yang dapat disesuaikan mungkin cukup menyenangkan, itu akan terasa lebih seperti aplikasi web atau demo standar daripada game nyata dan lengkap. Jadi, untuk mempertahankan nuansa aplikasi asli tersebut, Quina melacak riwayat game Anda, menunjukkan statistik permainan Anda dalam berbagai cara, dan menawarkan beberapa "penghargaan" untuk berbagai pencapaian.

Quina melacak hasil dari semua game yang dimainkan, rentetan kemenangan terpanjang Anda, dan banyak statistik lainnya

Di bawah tenda, setiap game disimpan sebagai objek yang terlihat seperti ini:

 { guessesUsed: 3, difficulty: 'tricky', win: true, hint: 'none', }

Aplikasi membuat katalog game yang Anda mainkan (sekali lagi, melalui status Vuex yang disinkronkan ke localStorage ) dalam bentuk gameHistory dari objek game, yang kemudian digunakan aplikasi untuk menampilkan statistik Anda – seperti rasio menang / kalah, berapa banyak game yang Anda miliki. yang telah Anda mainkan, dan tebakan rata-rata Anda – serta untuk menunjukkan kemajuan Anda menuju "penghargaan" game.

Ini semua dilakukan dengan cukup mudah dengan berbagai getter Vuex, yang masing-masing menggunakan metode array JavaScript, seperti .filter() dan .reduce() , pada array gameHistory Misalnya, ini adalah getter yang menunjukkan berapa banyak game yang dimenangkan pengguna saat bermain di setelan "rumit":

 // store/getters.js trickyGamesWon(state) { return state.gameHistory.filter( (game) => game.win && game.difficulty === 'tricky' ).length },

Ada banyak pengambil lain dengan kompleksitas yang berbeda-beda. (Yang menentukan rentetan kemenangan terpanjang pengguna sangat menjijikkan.)

Menambahkan penghargaan adalah masalah membuat serangkaian objek penghargaan, masing-masing terkait dengan pengambil Vuex tertentu, dan masing-masing dengan requirement.threshold menunjukkan kapan penghargaan itu dibuka (yaitu, ketika nilai yang dikembalikan oleh pengambil cukup tinggi). Berikut contohnya:

 // assets/js/awards.js export default [ { title: 'Onset', requirement: { getter: 'totalGamesPlayed', threshold: 1, text: 'Play your first game of Quina', } }, { title: 'Sharp', requirement: { getter: 'trickyGamesWon', threshold: 10, text: 'Win ten total games on Tricky', }, }, ]

Dari sana, cukup mudah untuk mengulang pencapaian dalam file template Vue untuk mendapatkan hasil akhir, menggunakan requirement.text (meskipun ada banyak matematika dan animasi yang ditambahkan untuk mengisi pengukur guna menunjukkan kemajuan pengguna. menuju pencapaian penghargaan):

Penghargaan dibuka dan ditampilkan jika kemajuan pemain di atas ambang batas; jika tidak, permainan akan menampilkan bilah kemajuan dengan indikasi berapa banyak yang tersisa hingga penghargaan dibuka.

Ada 25 penghargaan secara keseluruhan (yaitu 5 × 5, sesuai dengan temanya) untuk berbagai pencapaian seperti memenangkan sejumlah permainan, mencoba semua mode permainan, atau bahkan memenangkan permainan dalam tiga tebakan pertama Anda. (Yang itu disebut "Lucky" – sebagai tambahan telur Paskah kecil, nama setiap penghargaan juga merupakan kata kode potensial, yaitu, lima huruf tanpa pengulangan.)

Membuka penghargaan tidak melakukan apa pun kecuali memberi Anda hak untuk menyombongkan diri, tetapi beberapa di antaranya cukup sulit untuk dicapai. (Saya butuh beberapa minggu setelah merilis untuk mendapatkan semuanya!)

Pro dan kontra dari pendekatan ini

Ada banyak hal yang disukai tentang strategi "bangun sekali, terapkan di mana saja", tetapi juga memiliki beberapa kekurangan:

Pro

  • Anda hanya perlu menerapkan aplikasi toko Anda sekali. Setelah itu, semua pembaruan hanya dapat menjadi penerapan situs web. (Ini jauh lebih cepat daripada menunggu rilis toko aplikasi.)
  • Bangun sekali . Ini agak benar, tetapi ternyata tidak sesederhana yang saya kira karena kebijakan pembayaran Google (lebih lanjut nanti).
  • Semuanya adalah browser. Aplikasi Anda selalu berjalan di lingkungan yang biasa Anda gunakan, baik pengguna menyadarinya atau tidak.

Kontra

  • Penangan acara bisa menjadi sangat rumit. Karena kode Anda berjalan di semua platform secara bersamaan, Anda harus mengantisipasi semua jenis input pengguna sekaligus. Beberapa elemen dalam aplikasi dapat diketuk, diklik, ditekan lama, dan juga merespons secara berbeda ke berbagai tombol keyboard; bisa jadi sulit untuk menangani semuanya sekaligus tanpa ada pawang yang menginjak kaki satu sama lain.
  • Anda mungkin harus membagi pengalaman. Ini akan bergantung pada apa yang dilakukan aplikasi Anda, tetapi ada beberapa hal yang perlu saya tampilkan hanya untuk pengguna di aplikasi Android dan lainnya yang hanya untuk web. (Saya membahas sedikit lebih detail tentang bagaimana saya menyelesaikan ini di bagian lain di bawah.)
  • Semuanya adalah browser . Anda tidak khawatir tentang apa versi Android pengguna Anda, tetapi Anda khawatir tentang apa browser default mereka (karena aplikasi akan menggunakan browser default mereka di belakang layar). Biasanya pada Android ini akan berarti Chrome, tetapi Anda harus memperhitungkan setiap kemungkinan.

Logistik: mengubah aplikasi web menjadi aplikasi asli

Ada banyak teknologi di luar sana yang membuat janji "buat untuk web, rilis di mana-mana" – React Native , Cordova , Ionic , Meteor , dan NativeScript , hanya untuk beberapa nama.

Umumnya, ini bermuara pada dua kategori:

  1. Anda menulis kode sesuai keinginan framework (tidak persis seperti biasanya), dan framework mengubahnya menjadi aplikasi asli yang sah;
  2. Anda menulis kode Anda seperti biasa, dan teknisi hanya membungkus "shell" asli di sekitar teknologi web Anda dan pada dasarnya menyamarkannya sebagai aplikasi asli.

Pendekatan pertama mungkin tampak seperti yang lebih diinginkan dari keduanya (karena pada akhirnya Anda secara teoritis berakhir dengan aplikasi asli "nyata"), tetapi saya juga menemukan itu datang dengan rintangan terbesar. Setiap platform atau produk mengharuskan Anda mempelajari caranya melakukan sesuatu, dan cara itu terikat untuk menjadi keseluruhan ekosistem dan kerangka kerja tersendiri. Janji "tulis saja apa yang kamu tahu" adalah pernyataan berlebihan yang cukup kuat dalam pengalaman saya. Saya kira dalam satu atau dua tahun banyak masalah itu akan terpecahkan, tetapi saat ini, Anda masih merasakan celah yang cukup besar antara menulis kode web dan mengirimkan aplikasi asli.

Di sisi lain, pendekatan kedua dapat diterapkan karena sesuatu yang disebut "TWA", yang memungkinkan untuk membuat situs web menjadi aplikasi di tempat pertama.

Apa itu Aplikasi TWA?

TWA adalah singkatan dari Trusted Web Activity – dan karena jawaban itu sepertinya tidak membantu sama sekali, mari kita uraikan sedikit lagi, oke?

Aplikasi TWA pada dasarnya mengubah situs web (atau aplikasi web, jika Anda ingin membagi rambut) menjadi aplikasi asli, dengan bantuan sedikit tipuan UI.

Anda dapat menganggap aplikasi TWA sebagai browser yang menyamar . Ini adalah aplikasi Android tanpa internal apa pun, kecuali untuk browser web. Aplikasi TWA diarahkan ke URL web tertentu, dan setiap kali aplikasi di-boot, daripada melakukan hal-hal aplikasi asli normal, itu hanya memuat situs web itu – layar penuh, tanpa kontrol browser, secara efektif membuat situs web terlihat dan berperilaku sebagai meskipun itu adalah aplikasi asli yang lengkap.

Persyaratan TWA

Sangat mudah untuk melihat daya tarik membungkus situs web dalam aplikasi asli. Namun, tidak sembarang situs atau URL lama yang memenuhi syarat; untuk meluncurkan situs / aplikasi web Anda sebagai aplikasi asli TWA, Anda harus mencentang kotak berikut:

  • Situs / aplikasi Anda harus berupa PWA. Google menawarkan pemeriksaan validasi sebagai bagian dari Lighthouse , atau Anda dapat memeriksanya dengan Bubblewrap (lebih lanjut tentang itu sebentar lagi).
  • Anda harus membuat sendiri app bundle / APK; itu tidak semudah hanya mengirimkan URL aplikasi web progresif Anda dan menyelesaikan semua pekerjaan untuk Anda. (Jangan khawatir; kami akan membahas cara untuk melakukan ini meskipun Anda tidak tahu apa-apa tentang pengembangan aplikasi asli.)
  • Anda harus memiliki kunci aman yang cocok, baik di aplikasi Android dan diunggah ke aplikasi web Anda di URL tertentu.

Poin terakhir itu adalah di mana bagian "tepercaya" masuk; aplikasi TWA akan memeriksa kuncinya sendiri, kemudian memverifikasi bahwa kunci pada aplikasi web Anda cocok dengan itu, untuk memastikannya memuat situs yang benar (mungkin, untuk mencegah pembajakan URL aplikasi yang berbahaya). Jika kuncinya tidak cocok atau tidak ditemukan, aplikasi akan tetap berfungsi, tetapi fungsionalitas TWA akan hilang; itu hanya akan memuat situs web di browser biasa, chrome dan semua. Jadi, kuncinya sangat penting untuk pengalaman aplikasi. (Bisa dibilang itu adalah bagian penting . Maaf tidak, maaf.)

Keuntungan dan kerugian membangun aplikasi TWA

Keuntungan utama dari aplikasi TWA adalah Anda tidak perlu mengubah kode sama sekali – tidak ada kerangka kerja atau platform untuk dipelajari; Anda baru saja membuat situs web / aplikasi web seperti biasa, dan setelah Anda menyelesaikannya, pada dasarnya Anda juga telah menyelesaikan kode aplikasinya.

Namun, kelemahan utamanya adalah (meskipun membantu mengantarkan era modern web dan JavaScript), Apple tidak mendukung aplikasi TWA; Anda tidak dapat mencantumkannya di Apple App store. Hanya Google Play.

Ini mungkin terdengar seperti pemecah kesepakatan, tetapi ingatlah beberapa hal:

  • Ingat, untuk mencantumkan aplikasi Anda terlebih dahulu, itu harus berupa PWA – yang berarti dapat diinstal secara default. Pengguna di platform apa pun masih dapat menambahkannya ke layar beranda perangkat mereka dari browser. Itu tidak perlu berada di Apple App Store untuk diinstal pada perangkat Apple (meskipun tentu saja tidak dapat ditemukan). Jadi, Anda masih dapat membuat halaman landing pemasaran ke dalam aplikasi Anda dan meminta pengguna untuk menginstalnya dari sana.
  • Juga tidak ada yang mencegah Anda mengembangkan aplikasi iOS asli menggunakan strategi yang sama sekali berbeda. Bahkan jika Anda menginginkan aplikasi iOS dan Android, selama aplikasi web juga merupakan bagian dari rencana tersebut, memiliki TWA secara efektif memotong setengah dari pekerjaan itu.
  • Terakhir, sementara iOS memiliki sekitar 50% pangsa pasar di negara-negara yang sebagian besar berbahasa Inggris dan Jepang, Android menguasai lebih dari 90% negara lainnya di dunia. Jadi, bergantung pada audiens Anda, melewatkan App Store mungkin tidak berdampak seperti yang Anda kira.

Cara membuat APK Aplikasi Android

Pada titik ini Anda mungkin berkata, bisnis TWA ini terdengar bagus dan bagus, tetapi bagaimana saya benar-benar mengambil situs / aplikasi saya dan memasukkannya ke dalam aplikasi Android?

Jawabannya datang dalam bentuk alat CLI kecil yang bagus yang disebut Bubblewrap .

Anda dapat menganggap Bubblewrap sebagai alat yang mengambil beberapa masukan dan opsi dari Anda, dan menghasilkan aplikasi Android (khususnya, APK, salah satu format file yang diizinkan oleh Google Play Store) dari masukan.

Menginstal Bubblewrap sedikit rumit, dan meskipun menggunakannya tidak cukup plug-and-play, itu pasti jauh lebih mudah dijangkau oleh pengembang front-end rata-rata daripada opsi serupa lainnya yang saya temukan. File README di halaman NPM Bubblewrap menjelaskan detailnya, tetapi sebagai ikhtisar singkat:

Instal Bubblewrap dengan menjalankan npm i -g @bubblewrap/cli (Saya berasumsi di sini Anda sudah familiar dengan NPM dan menginstal paket darinya melalui baris perintah). Itu akan memungkinkan Anda menggunakan Bubblewrap di mana saja.

Setelah terinstal, Anda akan menjalankan:

 bubblewrap init --manifest https://your-webapp-domain/manifest.json

Catatan: manifest.json diperlukan untuk semua PWA, dan Bubblewrap memerlukan URL ke file tersebut, bukan hanya aplikasi Anda. Juga berhati-hatilah: bergantung pada bagaimana file manifes Anda dibuat, namanya mungkin unik untuk setiap build. (Modul PWA Nuxt menambahkan UUID unik ke nama file, misalnya.)

Perhatikan juga bahwa secara default, Bubblewrap akan memvalidasi bahwa aplikasi web Anda adalah PWA yang valid sebagai bagian dari proses ini. Untuk beberapa alasan, ketika saya menjalani proses ini, cek terus negatif, meskipun Lighthouse mengonfirmasi bahwa itu sebenarnya adalah aplikasi web progresif yang berfungsi penuh. Untungnya, Bubblewrap memungkinkan Anda melewati pemeriksaan ini dengan flag --skipPwaValidation

Jika ini adalah pertama kalinya Anda menggunakan Bubblewrap, ia akan menanyakan apakah Anda ingin menginstal Java Development Kit (JDK) dan Android Software Development Kit (SDK) untuk Anda. Keduanya adalah utilitas di balik layar yang diperlukan untuk menghasilkan aplikasi Android. Jika Anda tidak yakin, tekan "Y" untuk ya.

Catatan: Bubblewrap mengharapkan kedua kit pengembangan ini ada di lokasi yang sangat spesifik , dan tidak akan berfungsi dengan baik jika tidak ada. Anda dapat menjalankan bubblewrap doctor untuk memverifikasi, atau melihat Bubblewrap CLI README selengkapnya .

Setelah semuanya diinstal – anggap manifest.json Anda ditemukan di URL yang disediakan – Bubblewrap akan menanyakan beberapa pertanyaan tentang aplikasi Anda.

Banyak pertanyaan yang merupakan preferensi (seperti warna utama aplikasi Anda) atau hanya mengonfirmasi detail dasar (seperti domain dan titik masuk untuk aplikasi), dan sebagian besar akan diisi sebelumnya dari file manifes situs Anda.

Pertanyaan lain yang mungkin sudah diisi sebelumnya oleh manifes Anda termasuk di mana menemukan berbagai ikon aplikasi Anda (untuk digunakan sebagai ikon layar beranda, ikon bilah status, dll.), Apa warna splash screen saat aplikasi dibuka, dan orientasi layar aplikasi, jika Anda ingin memaksakan potret atau lanskap. Bubblewrap juga akan menanyakan apakah Anda ingin meminta izin untuk geolokasi pengguna Anda, dan apakah Anda ikut serta dalam Penagihan Play.

Namun, ada beberapa pertanyaan penting yang mungkin sedikit membingungkan, jadi mari kita bahas di sini:

  • ID Aplikasi: Tampaknya ini adalah konvensi Java, tetapi setiap aplikasi membutuhkan string ID unik yang umumnya 2–3 bagian yang dipisahkan titik (misalnya, collinsworth.quina.app ). Tidak masalah apa ini ; itu tidak berfungsi, itu hanya konvensi. Satu-satunya hal yang penting adalah Anda mengingatnya, dan itu unik. Tapi apakah catatan bahwa ini akan menjadi bagian dari aplikasi Anda unik Google Play Toko URL. (Karena alasan ini, Anda tidak dapat mengupload paket baru dengan ID Aplikasi yang digunakan sebelumnya, jadi pastikan Anda puas dengan ID Anda.)
  • Versi awal: Ini tidak masalah untuk saat ini, tetapi Play Store akan meminta Anda untuk menaikkan versi saat Anda mengunggah bundel baru, dan Anda tidak dapat mengunggah versi yang sama dua kali. Jadi saya akan merekomendasikan mulai dari 0 atau 1.
  • Mode tampilan : Sebenarnya ada beberapa cara aplikasi TWA dapat menampilkan situs Anda . Di sini, Anda kemungkinan besar ingin memilih standalone (layar penuh, tetapi dengan bilah status asli di bagian atas), atau fullscreen (tanpa bilah status). Saya pribadi memilih standalone default, karena saya tidak melihat alasan apa pun untuk menyembunyikan bilah status pengguna di dalam aplikasi, tetapi Anda mungkin memilih secara berbeda tergantung pada apa yang dilakukan aplikasi Anda.

Kunci penandatanganan

Bagian terakhir dari teka-teki itu adalah kunci penandatanganan. Ini bagian terpenting . Kunci inilah yang menghubungkan aplikasi web progresif Anda ke aplikasi Android ini. Jika kunci aplikasi mengharapkan tidak sesuai apa yang ditemukan di PWA Anda, sekali lagi: aplikasi Anda akan tetap bekerja, tetapi tidak akan terlihat seperti aplikasi asli ketika pengguna membuka itu; itu hanya akan menjadi jendela browser biasa.

Ada dua pendekatan di sini yang agak terlalu rumit untuk dijelaskan secara mendetail, tetapi saya akan mencoba memberikan beberapa petunjuk:

  1. Buat keystore Anda sendiri . Anda dapat meminta Bubblewrap melakukan ini, atau menggunakan alat CLI yang disebut keytool (cukup tepat), tetapi bagaimanapun caranya: berhati-hatilah . Anda perlu secara eksplisit melacak nama dan kata sandi yang tepat untuk keystore Anda, dan karena Anda membuat keduanya pada baris perintah, Anda harus sangat berhati-hati terhadap karakter khusus yang dapat mengacaukan keseluruhan proses. (Karakter khusus dapat ditafsirkan secara berbeda pada baris perintah, bahkan ketika dimasukkan sebagai bagian dari prompt kata sandi.)
  2. Izinkan Google menangani kunci Anda. Sejujurnya ini tidak secara dramatis lebih sederhana menurut pengalaman saya, tetapi ini menyelamatkan beberapa masalah dari pertengkaran kunci penandatanganan Anda sendiri dengan memungkinkan Anda masuk ke konsol Pengembang Google Play, dan mengunduh kunci yang dibuat sebelumnya untuk aplikasi Anda.

Opsi mana pun yang Anda pilih, ada dokumentasi mendalam tentang penandatanganan aplikasi di sini (ditulis untuk aplikasi Android, tetapi sebagian besar masih relevan).

Bagian di mana Anda mendapatkan kunci ke situs pribadi Anda tercakup dalam panduan ini untuk memverifikasi tautan aplikasi Android . Untuk meringkas secara kasar: Google akan mencari /.well-known/assetlinks.json di jalur yang tepat di situs Anda. File tersebut harus berisi hash kunci unik Anda serta beberapa detail lainnya:

 [{ "relation": ["delegate_permission/common.handle_all_urls"], "target" : { "namespace": "android_app", "package_name": "your.app.id", "sha256_cert_fingerprints": ["your:unique:hash:here"] } }]

Yang perlu Anda ketahui tentang mencantumkan aplikasi

Sebelum Anda memulai, ada juga beberapa rintangan yang harus diperhatikan di sisi app store:

  • Pertama dan terpenting, Anda perlu mendaftar sebelum Anda dapat memublikasikan ke Google Play Store. Kelayakan ini membutuhkan biaya $ 25 USD satu kali.
  • Setelah disetujui, ketahuilah bahwa membuat daftar aplikasi tidaklah cepat atau mudah. Ini lebih membosankan daripada sulit atau teknis, tetapi Google meninjau setiap aplikasi dan pembaruan di toko, dan mengharuskan Anda untuk mengisi banyak formulir dan info tentang diri Anda dan aplikasi Anda bahkan sebelum Anda dapat memulai proses peninjauan – yang dengan sendirinya dapat memakan waktu berhari-hari, bahkan jika aplikasi Anda belum menjadi publik. (Perhatian ramah: sudah ada spanduk peringatan "kami mengalami waktu peninjauan lebih lama dari biasanya" di dasbor konsol Play setidaknya selama enam bulan sekarang.)
    • Di antara bagian yang lebih membosankan: Anda harus mengupload beberapa gambar aplikasi Anda sebelum peninjauan Anda dapat dimulai. Ini pada akhirnya akan menjadi gambar yang ditampilkan di listingan toko – dan perlu diingat bahwa mengubahnya juga akan memulai ulasan baru, jadi buka tabel yang disiapkan jika Anda ingin meminimalkan waktu penyelesaian.
    • Anda juga perlu memberikan tautan ke persyaratan layanan dan kebijakan privasi aplikasi Anda (yang merupakan satu-satunya alasan aplikasi saya memilikinya, karena semuanya tidak berguna).
    • Ada banyak hal yang tidak dapat Anda urungkan . Misalnya, Anda tidak pernah dapat mengubah aplikasi gratis menjadi berbayar, meskipun aplikasi tersebut belum diluncurkan secara publik dan / atau tidak memiliki unduhan sama sekali. Anda juga harus ketat dalam menentukan versi dan penamaan dengan apa yang Anda unggah, karena Google tidak mengizinkan Anda menimpa atau menghapus aplikasi atau paket yang Anda unggah, dan juga tidak selalu membiarkan Anda mengembalikan pengaturan lain di dasbor. Jika Anda memiliki pendekatan "langsung saja masuk dan kerjakan nanti" (seperti saya), Anda mungkin menemukan diri Anda memulai dari awal setidaknya sekali atau dua kali.
  • Dengan beberapa pengecualian, Google memiliki kebijakan yang sangat ketat tentang pengumpulan pembayaran di aplikasi. Ketika saya sedang membangun, itu membebankan biaya 30% untuk semua transaksi (mereka sejak itu secara bersyarat menurunkannya menjadi 15% – lebih baik, tetapi masih lima kali lebih banyak daripada kebanyakan penyedia pembayaran lainnya). Google juga memaksa pengembang (dengan beberapa pengecualian) untuk menggunakan platform pembayaran aslinya sendiri; tidak ada pilihan untuk Square, Stripe, PayPal, dll dalam aplikasi.
    • Fakta menyenangkan: kebijakan ini telah diumumkan tetapi belum berlaku saat saya mencoba melepaskan Quina, dan masih ditandai oleh peninjau karena melanggar. Jadi mereka pasti menganggap kebijakan ini dengan sangat serius.

Monetisasi, unlockable, dan berkeliling Google

Sementara tujuan saya dengan Quina sebagian besar bersifat pribadi – menantang diri saya sendiri, membuktikan bahwa saya bisa, dan mempelajari lebih lanjut tentang ekosistem Vue dalam aplikasi dunia nyata yang kompleks – saya juga berharap sebagai tujuan sekunder bahwa pekerjaan saya mungkin dapat menghasilkan sedikit uang di samping untuk saya dan keluarga saya.

Tidak banyak. Saya tidak pernah memiliki ilusi untuk membangun Candy Crush berikutnya (atau kekosongan etika yang diperlukan untuk merekayasa mesin transaksi mikro yang memicu kecanduan). Tetapi karena saya telah mencurahkan ratusan jam waktu dan energi saya ke dalam permainan, saya berharap bahwa mungkin saya dapat menghasilkan sesuatu sebagai imbalan, meskipun itu hanya sedikit uang bir.

Awalnya, saya tidak menyukai ide untuk mencoba menjual aplikasi atau mengunci kontennya, jadi saya memutuskan untuk menambahkan kalimat sederhana "maukah Anda mendukung Quina jika Anda menyukainya?" prompt setelah begitu banyak game, dan buat beberapa konten tidak dapat dibuka khusus untuk pendukung. (Kumpulan kata terbatas dalam ukuran bu default, dan beberapa pengaturan permainan pada awalnya dikunci juga.) Permintaan untuk mendukung Quina dapat ditutup secara permanen (Saya bukan monster), dan sumbangan apa pun membuka semuanya; tidak ada akses atau manfaat berjenjang.

Ini semua cukup mudah untuk diterapkan berkat Stripe, bahkan tanpa server; itu semua sepenuhnya dari sisi klien. Saya baru saja mengimpor sedikit JavaScript di halaman /support , menggunakan head praktis Nuxt (yang menambahkan item ke elemen <head> secara khusus pada halaman yang diberikan):

 // pages/support.vue head() { return { script: [ { hid: 'stripe', src: 'https://js.stripe.com/v3', defer: true, callback: () => { // Adds all Stripe methods like redirectToCheckout to page component this.stripe = Stripe('your_stripe_id') }, }, ], } },

Dengan bit tersebut (bersama dengan taburan template dan logika), pengguna dapat memilih jumlah donasi mereka – disiapkan sebagai produk di sisi Stripe – dan dialihkan ke Stripe untuk menyelesaikan pembayaran, lalu dikembalikan setelah selesai. Untuk setiap tingkat, URL pengalihan kembali sedikit berbeda melalui parameter kueri. Vue Router mem-parsing URL untuk menyesuaikan riwayat donasi yang disimpan pengguna, dan membuka fitur yang sesuai.

Anda mungkin bertanya-tanya mengapa saya mengungkapkan semua ini, karena ini memperlihatkan sistem yang cukup mudah untuk merekayasa balik. Jawabannya adalah: Saya tidak peduli . Faktanya, saya sendiri menambahkan tingkat gratis, jadi Anda bahkan tidak perlu bersusah payah. Saya memutuskan bahwa jika seseorang benar -benar menginginkan unlockable tetapi tidak dapat atau tidak mau membayar untuk alasan apa pun, tidak apa-apa. Mungkin mereka hidup dalam situasi di mana $ 3 adalah uang yang banyak. Mungkin mereka sudah menyerah pada satu perangkat. Mungkin mereka akan melakukan hal lain yang baik. Tapi sejujurnya, meski niat mereka tidak baik: lalu apa?

Saya menghargai dukungan, tetapi ini bukan hidup saya, dan saya tidak sedang mencoba membangun loket tol dopamin. Selain itu, saya secara pribadi tidak nyaman dengan implikasi etis dari menggunakan setumpuk perangkat lunak open-source dan / atau gratis (belum lagi segunung dokumentasi, posting blog, dan jawaban Stack Overflow yang ditulis tentang semua itu) ke membangun taman tertutup untuk keuntungan pribadi.

Jadi, jika Anda menyukai Quina dan dapat mendukungnya: dengan tulus, terima kasih . Itu sangat berarti bagi saya. Saya senang melihat pekerjaan saya dinikmati. Tetapi jika tidak: itu keren. Jika Anda menginginkan opsi "gratis", itu ada untuk Anda.

Bagaimanapun, seluruh rencana ini menemui hambatan ketika saya mengetahui tentang kebijakan monetisasi baru Google Play, yang berlaku efektif tahun ini. Anda dapat membacanya sendiri , tetapi untuk meringkas: jika Anda menghasilkan uang melalui aplikasi Google Play dan bukan lembaga nonprofit, Anda harus membuka Google Pay dan membayar biaya yang mahal – Anda tidak diizinkan menggunakan penyedia pembayaran lain.

Ini berarti saya bahkan tidak dapat mencantumkan aplikasinya; itu akan diblokir hanya karena memiliki halaman "dukungan" dengan pembayaran yang tidak melalui Google. (Saya kira saya mungkin bisa mengatasi masalah ini dengan mendaftarkan organisasi nirlaba, tetapi tampaknya itu cara yang salah untuk melakukannya, pada sejumlah tingkatan.)

Solusi akhir saya adalah menagih aplikasi itu sendiri di Google Play, dengan mencantumkannya seharga $ 2,99 (bukan harga “gratis” yang saya rencanakan sebelumnya), dan cukup mengubah pengalaman aplikasi untuk pengguna Android yang sesuai.

Menyesuaikan pengalaman aplikasi untuk Google Play

Untungnya, aplikasi Android mengirimkan header khusus dengan ID unik aplikasi saat meminta situs web. Dengan menggunakan tajuk ini, cukup mudah untuk membedakan pengalaman aplikasi di web dan di aplikasi Android yang sebenarnya.

Untuk setiap permintaan, aplikasi memeriksa ID Android; jika ada, aplikasi menyetel boolean status isAndroid disebut isAndroid ke true . Status ini mengalir di seluruh aplikasi, bekerja untuk memicu berbagai persyaratan untuk melakukan hal-hal seperti menyembunyikan dan menampilkan berbagai pertanyaan FAQ, dan (yang paling penting) untuk menyembunyikan halaman dukungan di menu navigasi. Itu juga membuka kunci semua konten secara default (karena pengguna sudah "menyumbang" di Android, dengan membeli). Saya bahkan melangkah lebih jauh dengan membuat <WebOnly> dan <AndroidOnly> untuk membungkus konten yang hanya dimaksudkan untuk salah satu dari keduanya. (Jelas, pengguna Android yang tidak dapat mengunjungi halaman dukungan tidak boleh melihat FAQ tentang topik tersebut, sebagai contoh.)

 <!-- /src/components/AndroidOnly.vue --> <template> <div v-if="isAndroid"> <slot /> </div> </template> <script> export default { computed: { isAndroid() { return this.$store.state.isAndroid }, }, } </script>

Akuntansi untuk akun

Untuk beberapa saat saat membangun Quina, saya meminta Firebase untuk masuk dan menyimpan data pengguna. Saya sangat menyukai ide untuk memungkinkan pengguna bermain di semua perangkat mereka dan melacak statistik mereka di mana saja, daripada memiliki riwayat terpisah di setiap perangkat / browser.

Pada akhirnya, bagaimanapun, saya membatalkan ide itu, karena beberapa alasan. Salah satunya adalah kompleksitas; tidak mudah memelihara sistem akun dan database yang aman, bahkan dengan sistem yang bagus seperti Firebase, dan pengeluaran tambahan semacam itu bukanlah sesuatu yang saya anggap enteng. Tapi yang utama: keputusan itu bermuara pada keamanan dan kesederhanaan.

Pada akhirnya, saya tidak ingin bertanggung jawab atas data pengguna. Privasi dan keamanan mereka dijamin dengan menggunakan localStorage , dengan biaya portabilitas yang kecil. Saya harap para pemain tidak mempermasalahkan kemungkinan kehilangan statistik mereka dari waktu ke waktu jika itu berarti mereka tidak memiliki login atau data yang perlu dikhawatirkan. (Dan hei, itu juga memberi mereka kesempatan untuk mendapatkan penghargaan itu lagi.)

Plus, rasanya menyenangkan. Sejujurnya saya mengatakan bahwa tidak mungkin aplikasi saya dapat membahayakan keamanan atau data Anda karena ia tidak tahu apa-apa tentang Anda. Dan juga, saya tidak perlu khawatir tentang kepatuhan atau peringatan cookie atau semacamnya.

Membungkus

Membangun Quina adalah proyek saya yang paling ambisius hingga saat ini, dan saya bersenang-senang merancang dan merekayasa itu seperti yang saya lihat para pemain menikmatinya.

Saya harap perjalanan ini bermanfaat bagi Anda! Meskipun mendapatkan aplikasi web yang terdaftar di Google Play Store memiliki banyak langkah dan potensi jebakan, itu pasti dalam jangkauan pengembang front-end. Saya harap Anda mengambil cerita ini sebagai inspirasi, dan jika Anda melakukannya, saya senang melihat apa yang Anda bangun dengan pengetahuan yang baru Anda temukan.


Pos Apa yang Saya Pelajari Membangun Aplikasi Game Kata Dengan Nuxt di Google Play muncul pertama kali di CSS-Tricks .

Anda dapat mendukung CSS-Tricks dengan menjadi Supporter MVP .

May 25, 2021

codeorayo

Ampuh! Ini rahasia mengembangkan aplikasi secara instan, tinggal download dan kembangkan. Gabung sekarang juga! Premium Membership [PRIVATE] https://premium.codeorayo.com

Leave a Reply

Your email address will not be published. Required fields are marked *