<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lê Vũ Huy</title>
    <description>The latest articles on DEV Community by Lê Vũ Huy (@huylv).</description>
    <link>https://dev.to/huylv</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1132943%2Fce2dec11-cedd-46ce-b9a0-f3259eff15d6.png</url>
      <title>DEV Community: Lê Vũ Huy</title>
      <link>https://dev.to/huylv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/huylv"/>
    <language>en</language>
    <item>
      <title>Tích hợp GA4 Google Analytics vào Zalo MiniApp</title>
      <dc:creator>Lê Vũ Huy</dc:creator>
      <pubDate>Tue, 17 Mar 2026 13:14:44 +0000</pubDate>
      <link>https://dev.to/huylv/tich-hop-ga4-google-analytics-vao-zalo-miniapp-47gg</link>
      <guid>https://dev.to/huylv/tich-hop-ga4-google-analytics-vao-zalo-miniapp-47gg</guid>
      <description>&lt;p&gt;Hệ thống SaaS &lt;a href="https://evotech.vn/" rel="noopener noreferrer"&gt;Evotech&lt;/a&gt; bên mình đã chạy được gần 1 năm, khách hàng có doanh nghiệp lên đến hàng nghìn lượt truy cập mỗi ngày, tuy nhiên vẫn chưa có 1 cơ chế tracking đầy đủ để tracking user behavior, lượt truy cập, revenue,... Chính vì vậy, team mình đã tìm các giải pháp tracking đang có trên thị trường. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Posthog Self-hosted&lt;/strong&gt;&lt;br&gt;
Mình có thử qua &lt;a href="https://posthog.com/docs/self-host" rel="noopener noreferrer"&gt;Posthog self-hosted&lt;/a&gt; nhưng triển khai server khá phức tạp, tài liệu ko rõ ràng, và bản thân team Posthog thông báo công khai là không có customer support cho self-hosted nên đành phải quay xe, mặc dù tính năng của Posthog rất hay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Google Analytics&lt;/strong&gt;&lt;br&gt;
Dạo qua 1 vòng Community của miniapp thì thấy ae hay dùng GA4, tuy nhiên các câu hỏi Q&amp;amp;A đã outdate, và Zalo cũng ko có tài liệu chính thức, vì vậy mình tổng hợp lại các bước dưới đây cho anh em tham khảo. Hiện tại cách duy nhất gửi dữ liệu lên GA4 là thông qua gọi API Measurement Protocol. Thư viện &lt;a href="https://www.npmjs.com/package/zmp-ga4" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/zmp-ga4&lt;/a&gt; đã deprecated.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Tạo API Key&lt;br&gt;
Truy cập Google Analytics trên Google Console -&amp;gt; Admin -&amp;gt; Data streams -&amp;gt; Add stream -&amp;gt; Web -&amp;gt; Copy API Key&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdlw7668pohu2xslota4k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdlw7668pohu2xslota4k.png" alt=" " width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Gắn vào mini app&lt;br&gt;
Tạo 1 file Tracking.ts dùng chung cho việc xử lý event tracking:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// utils/Tracking.ts - Measurement Protocol cho Zalo Mini App&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MEASUREMENT_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;G-XXXXXXXXXX&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Thay bằng ID của bạn&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_API_SECRET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// Thay secret&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendGA4Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Lấy Zalo user ID làm client_id (unique tracking)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zaloUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;zmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCurrentUserInfo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clientId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;zaloUser&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;`zalo_anonymous_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;user_properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentUserProperties&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Auto params&lt;/span&gt;
          &lt;span class="na"&gt;page_title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;zmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentPage&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Zalo Mini App&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;page_location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`zalo://miniapp&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;engagement_time_msec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Tăng retention&lt;/span&gt;
          &lt;span class="c1"&gt;// Custom params&lt;/span&gt;
          &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://www.google-analytics.com/mp/collect?measurement_id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;MEASUREMENT_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;api_secret=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_SECRET&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GA4 send fail:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GA4 error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Export functions dùng&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GA4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;pageView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageTitle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sendGA4Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page_view&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;page_title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pageTitle&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;buttonClick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buttonName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sendGA4Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button_click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;button_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;buttonName&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;VND&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sendGA4Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;purchase&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currency&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="c1"&gt;// Thêm events khác...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kiểm tra network log, nếu có API này response 204 là thành công rồi&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvxne3rs1kir8rq9mu43s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvxne3rs1kir8rq9mu43s.png" alt=" " width="800" height="196"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kết quả trên trang Realtime Analytics trên Firebase&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffksb2gtvdcx9q32yeopp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffksb2gtvdcx9q32yeopp.png" alt=" " width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nếu có khó khăn gì thì &lt;a href="https://www.facebook.com/huylv.177" rel="noopener noreferrer"&gt;nhắn mình&lt;/a&gt; support nha :D &lt;/p&gt;

</description>
      <category>zalo</category>
      <category>miniapp</category>
      <category>ga4</category>
    </item>
    <item>
      <title>Hướng dẫn tích hợp CheckoutSDK vào Zalo Mini App (COD)</title>
      <dc:creator>Lê Vũ Huy</dc:creator>
      <pubDate>Sun, 14 Jul 2024 09:37:35 +0000</pubDate>
      <link>https://dev.to/huylv/huong-dan-tich-hop-checkout-sdk-zalo-cod-4b5j</link>
      <guid>https://dev.to/huylv/huong-dan-tich-hop-checkout-sdk-zalo-cod-4b5j</guid>
      <description>&lt;p&gt;Mình viết bài này để ghi lại quá trình tích hợp &lt;a href="https://mini.zalo.me/docs/payment/" rel="noopener noreferrer"&gt;Checkout SDK&lt;/a&gt; của Zalo. Không rõ các bạn ở Zalo bị dí deadline nhiều không :D mà tài liệu hướng dẫn của các bạn hơi thiếu thốn, không được cập nhật thường xuyên. Nhiều thông tin phải search ở trên trang community của Zalo chứ doc cũng không có luôn. Hi vọng bài này có thể giúp các bạn né được các pain point mà mình đã trải qua.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nhớ là phải đọc hết bài trước khi đặt câu hỏi nhé&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Bài viết này mình sẽ hướng dẫn với phương thức thanh toán COD trước.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mini.zalo.me/docs/payment/integration-setting/cod-setting/" rel="noopener noreferrer"&gt;Tham khảo tài liệu hướng dẫn của Zalo&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  1. Tạo đơn hàng bằng CheckoutSDK
&lt;/h1&gt;

&lt;p&gt;Giả sử rằng mini app của bạn đã hoàn thành xong chức năng tạo đơn hàng, giờ chúng ta cần gửi thông tin đơn hàng đó lên CheckoutSDK của Zalo. Để tạo được đơn hàng trên CheckoutSDK chúng ta cần tạo đơn hàng trên server của chúng ta, sau đó để nó ở trạng thái "Chờ thanh toán", rồi dùng ID của đơn hàng đó để định danh với CheckoutSDK.&lt;br&gt;
Cài đặt thư viện ở mini app:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"dependencies": {
    "zmp-sdk": "^2.39.1",
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Tạo đơn hàng:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Payment } from "zmp-sdk";

async function createOrder() {
    const cart = {
        items: [
            {
                product: { id: 1, name: "Xa phong", price: 10000},
                quantity: 2,
            }
        ],
        receiverName: "HuyLV"
        address: "toa nha FPT, so 10 Pham Van Bach",
        finalPrice: 20000,
    }
    const order = await createOrderOnYourServer(cart);

    // build data để tạo order trên CheckoutSDK
    const item = cart.items.map((item) =&amp;gt; ({
      id: String(item.product.id),
      amount: item.quantity * item.product.price,
    }));
    const paymentMethod = {
      id: "COD_SANDBOX",
      isCustom: false,
    };
    const extraData = {
      storeName: "Kho tổng",
      storeId: "1",
      orderId: order.id, // id mà chúng ta đã tạo ở server của mình
      notes: "",
    };
    const orderData: any = {
      desc: `Thanh toan ${cart.finalPrice}`,
      item,
      amount: body.finalPrice,
      extradata: JSON.stringify(extraData),
      method: JSON.stringify(paymentMethod),
    };
    // ở bước này cần tạo 1 chuỗi mac từ orderData để đảm bảo tính toàn vẹn của dữ liệu
    // để tạo được mac, chúng ta cần API ở bước 2
    const mac = await createMac(orderData);
    orderData.mac = mac;
    return new Promise((resolve, reject) =&amp;gt; {
        Payment.createOrder({
            ...orderData,
            success: resolve,
            fail: reject,
        });
    });
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Chú ý: Ở bước tạo mac của orderData, chúng ta bắt buộc phải gọi lên server để tạo mac chứ ko được tạo mac ở client side. Đoạn này &lt;a href="https://mini.zalo.me/docs/payment/createOrder/" rel="noopener noreferrer"&gt;tài liệu của Zalo&lt;/a&gt; có nhắc đến nhưng rất dễ bị miss.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fini1ee3kjt0btpvjifz5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fini1ee3kjt0btpvjifz5.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  2. Xây dựng các API payment phục vụ cho việc thanh toán
&lt;/h1&gt;

&lt;h3&gt;
  
  
  2.1. API tạo mac
&lt;/h3&gt;

&lt;p&gt;Mac là 1 string lưu thông tin xác thực của dữ liệu đơn hàng, dùng PrivateKey được cung cấp để chứng thực toàn bộ dữ liệu. Các dữ liệu được sắp xếp theo thứ tự từ điển tăng dần để tạo mã hash cho thông tin. Code API tạo mac như sau:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const CryptoJS = require('crypto-js');

createMac: async (body) =&amp;gt; {
    try {
      const dataMac = Object.keys(body)
        .sort()
        .map(
          (key) =&amp;gt;
            `${key}=${
              typeof body[key] === 'object'
                ? JSON.stringify(body[key])
                : body[key]
            }`
        )
        .join('&amp;amp;');
      // biến môi trường ZALO_CHECKOUT_SECRET_KEY lấy ở bước 3
      const mac = CryptoJS.HmacSHA256(
        dataMac,
        process.env.ZALO_CHECKOUT_SECRET_KEY
      ).toString();
      return mac;
    } catch (e) {
      console.log(e);
    }
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  2.2. API nhận dữ liệu PTTT
&lt;/h3&gt;

&lt;p&gt;Chúng ta cần build 1 API mở để server của Zalo gọi vào, và chúng ta sẽ phản hồi dữ liệu đó có toàn vẹn hay không. Đối với 2 phương thức: &lt;code&gt;Thanh toán khi nhận hàng (COD)&lt;/code&gt; và &lt;code&gt;Chuyển khoản ngân hàng&lt;/code&gt;, chúng ta cần hiện thực API này khi người dùng chọn một trong hai phương thức này.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Chú ý API này không require authentication nhé&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const CryptoJS = require('crypto-js');
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;

&lt;p&gt;zaloNotify: async (body) =&amp;gt; {&lt;br&gt;
    try {&lt;br&gt;
      const { data, mac } = body || {};&lt;br&gt;
      if (!data || !mac) {&lt;br&gt;
        return {&lt;br&gt;
          returnCode: 0,&lt;br&gt;
          returnMessage: 'Missing data or mac',&lt;br&gt;
        };&lt;br&gt;
      }&lt;br&gt;
      const { method, orderId, appId } = data || {};&lt;br&gt;
      if (!method || !orderId || !appId) {&lt;br&gt;
        return {&lt;br&gt;
          returnCode: 0,&lt;br&gt;
          returnMessage: 'Missing method or orderId or appId',&lt;br&gt;
        };&lt;br&gt;
      }&lt;br&gt;
      const str = &lt;code&gt;appId=${appId}&amp;amp;orderId=${orderId}&amp;amp;method=${method}&lt;/code&gt;;&lt;br&gt;
      const reqMac = CryptoJS.HmacSHA256(&lt;br&gt;
        str,&lt;br&gt;
        process.env.ZALO_CHECKOUT_SECRET_KEY&lt;br&gt;
      ).toString();&lt;br&gt;
      if (reqMac == mac) {&lt;br&gt;
        return {&lt;br&gt;
          returnCode: 1,&lt;br&gt;
          returnMessage: 'Success',&lt;br&gt;
        };&lt;br&gt;
      } else {&lt;br&gt;
        return {&lt;br&gt;
          returnCode: 0,&lt;br&gt;
          returnMessage: 'Fail',&lt;br&gt;
        };&lt;br&gt;
      }&lt;br&gt;
    } catch (e) {&lt;br&gt;
      console.log(e);&lt;br&gt;
      return {&lt;br&gt;
        returnCode: 0,&lt;br&gt;
        returnMessage: 'Fail',&lt;br&gt;
      };&lt;br&gt;
    }&lt;br&gt;
  }&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OK, deploy 2 API này lên test server nhé.

&amp;gt; Server của bạn cần whitelist cho danh sách địa chỉ IP của CheckoutSDK Server:
&amp;gt; 118.102.2.29
&amp;gt; 49.213.78.2

# 3. Cài đặt trên trang quản lý mini app
- Truy cập trang quản lý mini app: 
https://mini.zalo.me/developers -&amp;gt; Chọn ZaloApp -&amp;gt; chọn mini app -&amp;gt; ở menu bên trái chọn CheckoutSDK -&amp;gt; cấu hình chung
- Chỉnh `AppStatus` thành `ACTIVE`, sử dụng `Private Key` cho biến môi trường `ZALO_CHECKOUT_SECRET_KEY`
- Thêm phương thức thanh toán mới, chọn `Thanh toán khi nhận hàng - Sandbox`
 - Notify Url: điền API ở bước 2.2

 - Redirect path: đường dẫn mà mini app sẽ redirect đến khi thanh toán xong
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sblsv4hczgnjz6vwre2d.png)

# 4. Thêm màn hình kết quả thanh toán
Sau thanh toán xong thì chắc chắn là phải mở ra màn kết quả thanh toán rồi. Ở đây sẽ xảy ra 2 trường hợp phụ thuộc vào phiên bản Zalo mà user sử dụng.

1. Với phiên bản Zalo đã hỗ trợ event open app, bạn cần lắng nghe sự kiện OpenApp và kiểm tra kết quả từ hệ thống thanh toán để xử lý.
&amp;gt; iOS: từ 22.02.01
&amp;gt; Android: từ 22.03.02

2. Với những phiên bản Zalo chưa hỗ trợ event OpenApp, sau khi thanh toán, hệ thống sẽ điều hướng người dùng tới redirect path đã cấu hình trên web.

Để lắng nghe sự kiện open app, bạn cần khai báo hook `useHandlePayment` và gọi nó khi khởi tạo app.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;import { events, EventName } from "zmp-sdk";&lt;/p&gt;

&lt;p&gt;export const useHandlePayment = () =&amp;gt; {&lt;br&gt;
  const navigate = useNavigate();&lt;/p&gt;

&lt;p&gt;useEffect(() =&amp;gt; {&lt;br&gt;
    events.on(EventName.OpenApp, (data) =&amp;gt; {&lt;br&gt;
      // data.path = /checkout-result?env=DEVELOPMENT&amp;amp;version=zdev-655f4b9a&amp;amp;appTransID=240601_1923887902773438459351224118883&lt;br&gt;
      // checkout-result là path mà chúng ta khai báo ở bước 3&lt;br&gt;
      if (data?.path) {&lt;br&gt;
        navigate(data?.path, {&lt;br&gt;
          state: data,&lt;br&gt;
        });&lt;br&gt;
      }&lt;br&gt;
    });&lt;br&gt;
  });&lt;br&gt;
}&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Do thanh toán COD thì kết quả luôn là thành công rồi nên màn kết quả thanh toán cũng đơn giản thôi:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;// CheckoutResult.tsx&lt;br&gt;
export default function CheckoutResult() {&lt;br&gt;
  const navigate = useNavigate();&lt;/p&gt;


&lt;p&gt;return (&lt;br&gt;&lt;br&gt;
    &lt;/p&gt;
&lt;br&gt;&lt;br&gt;
      &lt;h1&gt;Thanh toán thành công&lt;/h1&gt;
&lt;br&gt;&lt;br&gt;
    &lt;br&gt;&lt;br&gt;
  );&lt;br&gt;&lt;br&gt;
}

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OK, everything well done, hãy build lên **máy thật** và chạy thử thôi nào. Phần thanh toán này chỉ có thể test trên máy thật thôi nhé.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/koibniji4o3buqhvsp4x.png)

# 5. Go live
Chú ý: lên môi trường production cần phải tạo thêm phương thức thanh toán `Thanh toán khi nhận hàng` (ko Sandbox).

Like ủng hộ mình để mình làm tiếp bài hướng dẫn tích hợp VNPAY nha :D

Nếu có bất kỳ thắc mắc nào, có thể comment bên dưới hoặc contact mình qua [đây](https://www.facebook.com/huylv.177), mình sẽ support bạn trong khả năng của mình ;)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>zalo</category>
      <category>checkoutsdk</category>
    </item>
    <item>
      <title>Install NVM, Yarn, Postgres, Nginx, PM2 on Centos 8</title>
      <dc:creator>Lê Vũ Huy</dc:creator>
      <pubDate>Sun, 14 Apr 2024 14:51:53 +0000</pubDate>
      <link>https://dev.to/huylv/install-nvm-yarn-on-centos-8-4egg</link>
      <guid>https://dev.to/huylv/install-nvm-yarn-on-centos-8-4egg</guid>
      <description>&lt;p&gt;These are the quick commands to install NVM, Nodejs, Yarn, Nginx on Centos 8&lt;/p&gt;

&lt;h2&gt;
  
  
  1. NVM
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo yum install curl tar nano -y 
curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash   
source ~/.bashrc
nvm install 18
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Yarn
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo
sudo rpm --import https://dl.yarnpkg.com/rpm/pubkey.gpg
sudo yum install yarn -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Nginx
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo yum install -y epel-release nginx
sudo systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Config firewall&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo yum install firewalld -y
sudo systemctl enable firewalld
sudo reboot

sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload
sudo systemctl enable nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Download:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo dnf module enable postgresql:13 -y
sudo dnf install postgresql-server -y
sudo postgresql-setup --initdb
sudo systemctl start postgresql
sudo systemctl enable postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setup password for user &lt;code&gt;postgres&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo -u postgres psql
ALTER USER postgres with password 'yourpassword';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open port 5432 to connect database from remote:&lt;br&gt;
&lt;code&gt;nano /var/lib/pgsql/data/postgresql.conf&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;listen_addresses = '*'
port = 5432 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open firewall port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;firewall-cmd --zone=public --add-port=5432/tcp --permanent
firewall-cmd --reload
systemctl restart firewalld
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open this file: &lt;code&gt;nano /var/lib/pgsql/data/pg_hba.conf&lt;/code&gt; and add this line to end of file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;host    all   all   0.0.0.0/0   md5
host    all   all   ::/0        md5

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart postgres&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl restart postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Unzip, git, pm2
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yum install -y unzip git
npm i -g pm2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Troubleshoot
&lt;/h1&gt;

&lt;p&gt;If you got this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(13: Permission denied) while connecting to upstream
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try to run this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;setsebool -P httpd_can_network_connect 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
    </item>
    <item>
      <title>Send email confirmation in Strapi v4</title>
      <dc:creator>Lê Vũ Huy</dc:creator>
      <pubDate>Sat, 30 Sep 2023 07:19:43 +0000</pubDate>
      <link>https://dev.to/huylv/send-email-confirmation-in-strapi-4anh</link>
      <guid>https://dev.to/huylv/send-email-confirmation-in-strapi-4anh</guid>
      <description>&lt;p&gt;I have spent 1 hour to search internet for this feature but encountered a few obstacles. So I write this tutorial, hope it helps someone 😄&lt;/p&gt;

&lt;p&gt;First, you need a SMTP mail server to send verification email. Luckily, Sendgrid provide this. In this tutorial, I will guide you to send email from your domain email, using Sendgrid as a mail server.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Verify your domain on Sendgrid
&lt;/h3&gt;

&lt;p&gt;Go to &lt;a href="https://app.sendgrid.com/settings/sender_auth"&gt;Sender authentication page&lt;/a&gt;, then click Authenticate Your Domain to start the process, they will ask you to add some DNS record to your domain. The result should look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ritP40bc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kupec997psekua2azh6b.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ritP40bc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kupec997psekua2azh6b.jpg" alt="Image description" width="800" height="211"&gt;&lt;/a&gt;&lt;br&gt;
Next, go to Setting -&amp;gt; API Keys and create a new API Key for this email.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Config SMTP on strapi
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;.env&lt;/code&gt; file in your strapi project, add&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;URL=http://localhost:1337
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;config strapi url:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// /config/server.js

module.exports = ({env}) =&amp;gt; {
    ...
    url: env("URL", "http://localhost:1337"),
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install &lt;a href="https://market.strapi.io/providers/@strapi-provider-email-nodemailer"&gt;email plugin&lt;/a&gt; using this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add @strapi/provider-email-nodemailer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add SMTP config to your &lt;code&gt;.env&lt;/code&gt; file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=465
SMTP_USERNAME=apikey
SMTP_PASSWORD=&amp;lt;your api key from step 1&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a file name &lt;code&gt;plugins.js&lt;/code&gt; in &lt;code&gt;config&lt;/code&gt; folder&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// /config/plugin.js

module.exports = ({ env }) =&amp;gt; ({
  email: {
    config: {
      provider: 'nodemailer',
      providerOptions: {
        host: env('SMTP_HOST', 'smtp.sendgrid.net'),
        port: env('SMTP_PORT', 465),
        auth: {
          user: env('SMTP_USERNAME'),
          pass: env('SMTP_PASSWORD'),
        },
      },
      settings: {
        defaultFrom: 'hello@example.com', 
        defaultReplyTo: 'hello@example.com',
      },
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;defaultFrom&lt;/code&gt; and &lt;code&gt;defaultReplyTo&lt;/code&gt; are email from your domain, it can be any email such as &lt;code&gt;support@yourdomain.com&lt;/code&gt;, &lt;code&gt;admin@yourdomain.com&lt;/code&gt;,...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn build
yarn develop --watch-admin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Test send mail
&lt;/h3&gt;

&lt;p&gt;Now go to admin dashboard on &lt;code&gt;http://localhost:8000&lt;/code&gt;, navigate to &lt;code&gt;Settings&lt;/code&gt; -&amp;gt; &lt;code&gt;Email plugin&lt;/code&gt; -&amp;gt; &lt;code&gt;Configuration&lt;/code&gt;, enter your email and &lt;code&gt;Send test email&lt;/code&gt;, if everything is setup correctly, you should receive an email 😎&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4kReXOSH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jvv5toytp2bia9jevh4p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4kReXOSH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jvv5toytp2bia9jevh4p.png" alt="Image description" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Send verification email
&lt;/h3&gt;

&lt;p&gt;You are almost done, now navigate to &lt;code&gt;Settings -&amp;gt; Advanced setting&lt;/code&gt;, turn on the &lt;code&gt;Enable email confirmation&lt;/code&gt;. Next, enter the &lt;code&gt;Redirection url&lt;/code&gt; below, it should be a page on your site to display the text &lt;code&gt;Congratulation! You have successfully confirmed your email&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--j4Ch_vT---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cr6th59440r65fz3mzm0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--j4Ch_vT---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cr6th59440r65fz3mzm0.png" alt="Image description" width="800" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Don't hesitate to ask me if you have any question 😁 &lt;a href="https://t.me/huylvz"&gt;@huylvz&lt;/a&gt;&lt;/p&gt;

</description>
      <category>strapi</category>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Extend core controller in Strapi v4</title>
      <dc:creator>Lê Vũ Huy</dc:creator>
      <pubDate>Fri, 29 Sep 2023 04:43:30 +0000</pubDate>
      <link>https://dev.to/huylv/extend-core-controller-in-strapi-v4-k2l</link>
      <guid>https://dev.to/huylv/extend-core-controller-in-strapi-v4-k2l</guid>
      <description>&lt;p&gt;Let's assume that you have a content type named &lt;strong&gt;Notification&lt;/strong&gt;, it store all the notification shows in Notification Screen. And you want to create an API to get only notification belongs to current user. Here is how to achieve this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Aeof2AS2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e3kbx9gycdiuq2nrgmqt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Aeof2AS2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e3kbx9gycdiuq2nrgmqt.png" alt="Image description" width="800" height="1422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This content type has some columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// src/api/notification/content-types/notification/schema.json
{
  "kind": "collectionType",
  "collectionName": "notifications",
  "info": {
    "singularName": "notification",
    "pluralName": "notifications",
    "displayName": "Notification"
  },
  "options": {
    "draftAndPublish": false
  },
  "pluginOptions": {},
  "attributes": {
    "content": {
      "type": "text",
      "required": true
    },
    "userId": {
      "type": "integer",
      "required": true
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After creating this content type on admin dashboard, source code should look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--5QPRGCyS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yz2l8ar3pwbsui5mxnza.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--5QPRGCyS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yz2l8ar3pwbsui5mxnza.png" alt="Image description" width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you want to create an API to get only notification belongs to current user. You do not need to create a new API using &lt;code&gt;npx strapi generate&lt;/code&gt;, just extend the generated controller.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Extend the core controller
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//src/api/notification/controllers/notification.js

const { createCoreController } = require('@strapi/strapi').factories;

module.exports = createCoreController(
  'api::notification.notification',
  ({ strapi }) =&amp;gt; ({
    async findMy(ctx) {
      const entries = await strapi
        .service('api::notification.notification')
        .find({
          filters: {
            userId: ctx.state.user.id,
          },
          sort: {
            createdAt: 'desc',
          },
        });
      ctx.body = entries;
    },
  })
);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Extend core router
&lt;/h3&gt;

&lt;p&gt;Create a file name my-notification.js in &lt;code&gt;src/api/notification/routes/&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// src/api/notification/routes/my-notification.js
module.exports = {
  routes: [
    {
      method: 'GET',
      path: '/notifications/my',
      handler: 'notification.findMy',
      config: {
        policies: [],
        middlewares: [],
      },
    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now go to admin dashboard, grant permission access this enpoint to Authenticated users&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BvTO8kP9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nwaomq4e6fkjygbg42ue.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BvTO8kP9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nwaomq4e6fkjygbg42ue.png" alt="Image description" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Test it
&lt;/h3&gt;

&lt;p&gt;Try to call this endpoint from Postman:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vEqVR4v1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sxj07fphypsl3ycn082f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vEqVR4v1--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sxj07fphypsl3ycn082f.png" alt="Image description" width="800" height="575"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Don't hesitate to ask me if you have any question :D &lt;a href="https://t.me/huylvz"&gt;@huylvz&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Upload image to an entry on Strapi</title>
      <dc:creator>Lê Vũ Huy</dc:creator>
      <pubDate>Thu, 21 Sep 2023 13:00:32 +0000</pubDate>
      <link>https://dev.to/huylv/upload-image-to-an-entry-on-strapi-1fbd</link>
      <guid>https://dev.to/huylv/upload-image-to-an-entry-on-strapi-1fbd</guid>
      <description>&lt;p&gt;Strapi is great but their document is really confusing. I spent about 2 hours trying to achieve this. When you create a new entry from REST API, you may also want to upload image to that entry. &lt;/p&gt;

&lt;h3&gt;
  
  
  1. REST API
&lt;/h3&gt;

&lt;p&gt;Try make a POST request to&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://STRAPI_URL/api/upload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with the form-data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ref: the table ID (ex: `api::submission.submission`)
refId: the entry ID
field: the field to upload
files: the file to upload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is how we do it in Postman&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hvmV4-of--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/psnyc9cvh81kgcjnd2l9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hvmV4-of--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/psnyc9cvh81kgcjnd2l9.png" alt="Postman" width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Javascript SDK
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/stun3r/strapi-sdk-js"&gt;strapi-js-sdk&lt;/a&gt; haven't supported upload file. Luckily, we can do it with a workaround.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const formData = new FormData();
formData.append(
    "files",
    blobFile,
    "file-name.jpg"
);
formData.append("ref", "api::submission.submission");
formData.append("refId", entryId);
formData.append("field", "speakingAudio");

const response = await strapi.request("POST", "/upload", {
    headers: {
        "Content-Type": "multipart/form-data",
    },
    data: formData,
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you have a question, don't hesitate to comment below.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Deploy Strapi trên Centos 7 sử dụng Nginx, PM2</title>
      <dc:creator>Lê Vũ Huy</dc:creator>
      <pubDate>Thu, 21 Sep 2023 12:23:27 +0000</pubDate>
      <link>https://dev.to/huylv/deploy-strapi-tren-centos-7-su-dung-nginx-pm2-1k32</link>
      <guid>https://dev.to/huylv/deploy-strapi-tren-centos-7-su-dung-nginx-pm2-1k32</guid>
      <description>&lt;h2&gt;
  
  
  1. Clone repo to VPS
&lt;/h2&gt;

&lt;h2&gt;
  
  
  2. Tạo file ecosystem.config.js trong project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module.exports = {
  apps: [
    {
      name: "strapi-app",
      script: "npm",
      args: "run develop",
      exp_backoff_restart_delay: 100,
    },
  ],
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Config domain cho project strapi: &lt;code&gt;./config/server.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export default ({ env }) =&amp;gt; ({
  host: env('HOST', '0.0.0.0'),
  port: env.int('PORT', 1337),
  url: 'http://admin.yourdomain.com',
  app: {
    keys: env.array('APP_KEYS'),
  },
  webhooks: {
    populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', false),
  },
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Chạy pm2 service
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i -g pm2
pm2 start ecosystem.config.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Cài đặt Nginx
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/huylv/cai-dat-cong-cu-tren-centos-7-4goa"&gt;Hướng dẫn&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Tạo file config nginx:
&lt;/h2&gt;

&lt;p&gt;Tạo file &lt;code&gt;/etc/nginx/con.f/loyalty-strapi.conf&lt;/code&gt; (thường sẽ đặt tên file theo tên app)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;upstream strapi {
    server 127.0.0.1:1337;
}

server {
    # Listen HTTP
    listen 80;
    listen [::]:80;
    server_name admin.yourdomain.com; 

        client_max_body_size 100M;

location / {
        proxy_pass http://strapi;

        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_pass_request_headers on;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bạn cần thay thế các thông tin domain của bạn vào field &lt;code&gt;server_name&lt;/code&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  6. Chạy nginx dưới user root:
&lt;/h2&gt;

&lt;p&gt;mở file &lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//change this
user nginx;

//to this
user root;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Restart nginx:
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sudo systemctl restart nginx&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Troubleshoot
&lt;/h2&gt;

&lt;h4&gt;
  
  
  1. Permission denied
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; connect() to 127.0.0.1:1337 failed (13: Permission denied) while connecting to upstream, client: 42.114.89.25, server: admin.yourdomain.com, request: "GET /favicon.ico HTTP/1.1", upstream: "http://127.0.0.1:1337/favicon.ico", host: "103.185.184.216", referrer: "http://103.185.184.216/"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ chạy lệnh&lt;/p&gt;

&lt;p&gt;&lt;code&gt;setsebool -P httpd_can_network_connect 1&lt;/code&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Install NVM, Nodejs, Yarn, Python3, GCC, Nginx, Postgres on Centos 7</title>
      <dc:creator>Lê Vũ Huy</dc:creator>
      <pubDate>Thu, 21 Sep 2023 11:57:23 +0000</pubDate>
      <link>https://dev.to/huylv/cai-dat-cong-cu-tren-centos-7-4goa</link>
      <guid>https://dev.to/huylv/cai-dat-cong-cu-tren-centos-7-4goa</guid>
      <description>&lt;p&gt;These are the quick commands to install NVM, Nodejs, Yarn, Python3, Gcc, Nginx on Centos 7&lt;/p&gt;

&lt;h2&gt;
  
  
  1. NVM
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo yum install curl nano -y 
curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash   
source ~/.bashrc
nvm install 16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Yarn
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo
sudo rpm --import https://dl.yarnpkg.com/rpm/pubkey.gpg
sudo yum install yarn -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Python 3
&lt;/h2&gt;

&lt;p&gt;If you want to install sqlite on Centos7, you will need python3 and gcc&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo yum install -y python3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. GCC
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yum install gcc-c++ -y
yum install cmake -y
yum install centos-release-scl -y
yum install devtoolset-9 -y

scl enable devtoolset-9 bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you restart the shell, it will revert to GCC 4.8.5.&lt;br&gt;
You can add &lt;code&gt;source /opt/rh/devtoolset-9/enable&lt;/code&gt; into you bashrc file to set GCC 9 as the default.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. Nginx
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo yum install -y epel-release
sudo yum install -y nginx
sudo systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Config firewall&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo yum install firewalld -y
sudo systemctl enable firewalld
sudo reboot
sudo firewall-cmd --state // check the firewall state
firewall-cmd --get-active-zones //check active zones

sudo firewall-cmd --permanent --zone=public --add-service=http
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload
sudo systemctl enable nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Download:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
sudo yum install -y postgresql15-server
sudo /usr/pgsql-15/bin/postgresql-15-setup initdb
sudo systemctl start postgresql-15
sudo systemctl enable postgresql-15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setup password for user &lt;code&gt;postgres&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo passwd postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open port 5432 to connect database from remote:&lt;br&gt;
&lt;code&gt;nano /var/lib/pgsql/15/data/postgresql.conf&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;listen_addresses = '*'
port = 5432 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open firewall port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;firewall-cmd --zone=public --add-port=5432/tcp --permanent
firewall-cmd --reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open this file: &lt;code&gt;nano /var/lib/pgsql/data/pg_hba.conf&lt;/code&gt; and add this line to end of file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;host  all  all 0.0.0.0/0 md5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart postgres&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl restart postgresql-15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Unzip
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yum install -y unzip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
    </item>
    <item>
      <title>Cài đặt alias trong dự án React Native</title>
      <dc:creator>Lê Vũ Huy</dc:creator>
      <pubDate>Thu, 21 Sep 2023 11:54:31 +0000</pubDate>
      <link>https://dev.to/huylv/cai-dat-alias-trong-du-an-react-native-5673</link>
      <guid>https://dev.to/huylv/cai-dat-alias-trong-du-an-react-native-5673</guid>
      <description>&lt;p&gt;Alias là dạng import rút gọn trong các dự án React, giúp chúng ta có code nhìn đẹp hơn. Bạn có thể import dạng @components/Button thay vì ../../components/Button. Các bước để cài đặt alias trong ứng dụng React Native khá đơn giản.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Cài đặt
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add --dev babel-plugin-module-resolver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Setup babel.config.js
&lt;/h2&gt;

&lt;p&gt;Định nghĩa các alias mà bạn sẽ sử dụng trong project, cái này sẽ giúp babel khi thông dịch code ts ra js thì sẽ hiểu dc các import alias.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module.exports = function (api) {
  api.cache(true);
  return {
    presets: ["babel-preset-expo"],
    plugins: [
      [
        "module-resolver",
        {
          alias: {
            utils: "./src/utils",
            screens: "./src/screens",
            components: "./src/components",
            recoilStates: "./src/recoil",
            navigator: "./src/navigator",
          },
        },
      ],
    ],
  };
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Setup tsconfig.json
&lt;/h2&gt;

&lt;p&gt;Định nghĩa lại 1 lần nữa ở bên tsconfig.json, cái này giúp typescript server hiểu được các import alias khi development.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "strict": true,
    "baseUrl": "src",
    "paths": {
      "utils/*": [
        "utils/*"
      ],
      "components/*": [
        "components/*"
      ],
      "screens/*": [
        "screens/*"
      ],
      "recoilStates/*": [
        "recoil/*"
      ],
      "navigator/*": [
        "navigator/*"
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Done 😄
&lt;/h2&gt;

&lt;p&gt;Hãy thử rebuild project của bạn và trải nghiệm, nhớ là mỗi khi thay đổi file babel.config.js thì bạn phải restart metro server nhé (react-native start —reset-cache hoặc expo start -c).&lt;/p&gt;

</description>
    </item>
    <item>
      <title>E2E test dự án React native sử dụng Detox</title>
      <dc:creator>Lê Vũ Huy</dc:creator>
      <pubDate>Sat, 16 Sep 2023 07:36:44 +0000</pubDate>
      <link>https://dev.to/huylv/e2e-test-du-an-react-native-su-dung-detox-22o4</link>
      <guid>https://dev.to/huylv/e2e-test-du-an-react-native-su-dung-detox-22o4</guid>
      <description>&lt;p&gt;Detox là 1 framework do Wix phát triển, dùng trong end to end testing (E2E), tức là test theo những thao tác mà end user thường thực hiện. E2E test tuy thời gian chạy lâu nhưng sẽ kiểm thử được sát nhất với những gì mà end user sẽ sử dụng trên app của chúng ta.&lt;/p&gt;

&lt;p&gt;Để bắt đầu E2E test trên React native:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Cài đặt
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install detox-cli --g
// for iOS
brew tap wix/brew
brew install applesimutils

yarn add "jest@^29" --dev
yarn add detox --dev
detox init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sau khi chạy dòng này, output thường sẽ là:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Created a file at path: .detoxrc.js 
Created a file at path: e2e/jest.config.js
Created a file at path: e2e/starter.test.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Setup cho iOS
&lt;/h2&gt;

&lt;p&gt;Replace all toàn bộ string &lt;code&gt;YOUR_APP&lt;/code&gt; trong file &lt;code&gt;.detoxrc.js&lt;/code&gt; thành tên dự án của bạn&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps: {
     'ios.debug': {
       type: 'ios.app',
       binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/YOUR_APP.app',
       build: 'xcodebuild -workspace ios/YOUR_APP.xcworkspace -scheme YOUR_APP -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build'
     },
     'ios.release': {
       type: 'ios.app',
       binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/YOUR_APP.app',
       build: 'xcodebuild -workspace ios/YOUR_APP.xcworkspace -scheme YOUR_APP -configuration Release -sdk iphonesimulator -derivedDataPath ios/build'
     },
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cũng trong file này, lựa chọn máy ảo để chạy test trên Android và iOS.&lt;/p&gt;

&lt;p&gt;Tên máy ảo sẽ có dạng iPhone 14 Pro và Pixel_4_API_31&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--UMzHvJv4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jbyj69j2vrm1lwwbmed8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--UMzHvJv4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jbyj69j2vrm1lwwbmed8.png" alt="Image description" width="606" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thêm script test vào package.json:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"scripts": {
    ...
    "pree2e-ios-debug": "detox build -c ios.sim.debug",
    "e2e-ios-debug": "detox test -c ios.sim.debug",
    "pree2e-android-debug": "detox build -c android.emu.debug",
    "e2e-android-debug": "detox test -c android.emu.debug"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chạy build debug:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yarn pree2e-ios-debug&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Chạy test:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yarn e2e-ios-debug&lt;/code&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cài đặt cho android&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thêm thư viện detox vào &lt;code&gt;android/build.gradle&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; buildscript {
   ext {
     buildToolsVersion = "31.0.0"
     minSdkVersion = 21 // (1)
     compileSdkVersion = 30
     targetSdkVersion = 30
+    kotlinVersion = 'X.Y.Z' // (2)
   }
 …
   dependencies {
     classpath("com.android.tools.build:gradle:7.1.1")
     classpath("com.facebook.react:react-native-gradle-plugin")
     classpath("de.undercouch:gradle-download-task:5.0.1")
+    classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") // (3)
 …

 allprojects {
   repositories {
     …
     google()
+    maven { // (4)
+      url("$rootDir/../node_modules/detox/Detox-android")
+    }
     maven { url 'https://www.jitpack.io' }
   }
 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bổ sung thư viện vào &lt;code&gt;android/app/build.gradle&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;android {
   …
   defaultConfig {
     …
     versionCode 1
     versionName "1.0"
+    testBuildType System.getProperty('testBuildType', 'debug')
+    testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
   …
   buildTypes {
     release {
       minifyEnabled true
       proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+      proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro"

       signingConfig signingConfigs.release
     }
   }
   …

 dependencies {
+  androidTestImplementation('com.wix:detox:+')
+  implementation 'androidx.appcompat:appcompat:1.1.0'
   implementation fileTree(dir: "libs", include: ["*.jar"])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tạo file hỗ trợ test tại đường dẫn: &lt;code&gt;android/app/src/androidTest/java/&amp;lt;your.package&amp;gt;/DetoxTest.java&lt;/code&gt;, chú ý là bạn phải tạo folder &lt;code&gt;androidTest&lt;/code&gt; nhé&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package com.&amp;lt;your.package&amp;gt;; // (1)

import com.wix.detox.Detox;
import com.wix.detox.config.DetoxConfig;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;

@RunWith(AndroidJUnit4.class)
@LargeTest
public class DetoxTest {
    @Rule // (2)
    public ActivityTestRule&amp;lt;MainActivity&amp;gt; mActivityRule = new ActivityTestRule&amp;lt;&amp;gt;(MainActivity.class, false, false);

    @Test
    public void runDetoxTests() {
        DetoxConfig detoxConfig = new DetoxConfig();
        detoxConfig.idlePolicyConfig.masterTimeoutSec = 90;
        detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60;
        detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60);

        Detox.runTests(mActivityRule, detoxConfig);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chạy build debug:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yarn pree2e-android-debug&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;chạy test:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;e2e-android-debug&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Done :D&lt;/p&gt;

&lt;p&gt;Nếu có vấn đề gì, đừng ngại comment hỏi mình bên dưới nhé!&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
