How to pass data between "THEME-APP-EXTENSION" and the backend of the app?

jameshagood
Shopify Partner
37 0 16

I am trying to figure out how to pass data between the theme-app-extension and the backend of the application that the theme-app-extension is connected to. The theme-app-extension is all liquid, css, and javascript so I wasn't sure if there was a built in way to pass data between the two. For example is there a suggested method to pull data into the theme-app-extension from the database and is there a suggested way to send data to the database from the code running the theme-app-extension? I am fairly new to doing anything with theme-app-extensions as well as building Shopify applications. I have built Shopify applications that was admin facing or just cosmetic, this is my first time building a Shopify application that takes user input and sends it to the database and retrieves that data for the end-user to see. 

 

Any suggestions would be greatly appreciated.

 

Thank You.

Replies 37 (37)

jameshagood
Shopify Partner
37 0 16

For anybody else reading this in the future with the same problem.

 

The answer is to use shopify app proxies, https://shopify.dev/apps/online-store/app-proxies

 

a rough outline of the steps I took to do this.

 

  1. Set up the proxy in the shopify dashboard
  2. Make a route in the app to handle the requests sent to that endpoint
  3. Save that data to the database of the application
jameshagood
Shopify Partner
37 0 16

I don't really know but the way I got it to work was just by putting the ngrok url (APP URL) in the App Proxy proxy url field in the APP SETUP panel. in the subpath prefix I put apps and in the subpath field I put the app name. That worked for me and I am able to send data from the theme-app-extensions to the backend api route and save the data into the database and retrieve that data for the shopify admin area dashboard. Let me know if this helps or not.

diego-navarro
Shopify Partner
20 1 13

Hello Jameshagood, your answers are very helpful.

 

The ngrok URL changes frequently, do you update the APP URL manually? If not, how do you handle this change? Do you think using the ngrok URL would work in production?

jameshagood
Shopify Partner
37 0 16

yes I update every time it changes which is like once a day. The app I am building isn't in production yet but in production you should just be able to put whatever the URL is for the app once it is on servers somewhere with a HTTPS URL

jameshagood
Shopify Partner
37 0 16

I have built a couple different shopify apps when I was trying to learn how to do it, so I have never put one in production. the one I am currently building will be my first live shopify application that I built. 

fportillo
Shopify Partner
2 0 2

This sounds very helpful! Exactly same question I had, I was wondering if you know anything about this but when I make a fetch request like shown in the video, it routes to a different ip address then ngrok even if I added that in the proxy configuration. Did you ever get that? I am perhaps starting my app wrong.

raultoks-peppy
Shopify Partner
10 1 4

Hello James, could you provide some insight into the naming of routes and the verification of incoming requests from the proxy. I have followed the same steps, I am able to send a request to the app-proxy but unable to get it through to the backend api. I receive no errors It simply doesnt connect to the backend.

jameshagood
Shopify Partner
37 0 16

This has been a little while ago since I done this. I looked at the code I had to see the routes I had set up and I had a POST route to /proxy

raghav_kanwal
Shopify Partner
4 0 1

@jameshagood Thanks for sharing your knowledge with the community, I am still facing the same issue and have posted the details here - https://github.com/Shopify/shopify-api-js/issues/802

Could you please have a look and if possible can you share snippet code which worked for you ?

Thanks

 

raultoks-peppy
Shopify Partner
10 1 4

I managed to get everything working by simply deploying the app to gcp, the proxy requests simply wouldn't go through for a ngrok url, initially they were blocked by ngroks default browser check but even disabling that changed nothing. So the routes were defined correctly only to begin with thus no blockers on that front.

Charles_Roberts
Shopify Partner
47 0 9

@jameshagood @raultoks-peppy 

 

Set-up:

I managed to get things working as well:

 

  1. Create App Proxy
  2. Create Remix App Bridge app
  3. Create a Theme App Extension, inside of the Remix App [add extension to project root, not app root]
  4. └── extensions
        └── my-theme-app-extension
            ├── assets/extension.js [this has the fetch() method in it]
            ├── blocks/extension.liquid [add form HTML in it]
            ├── snippets
            ├── locales
            ├── package.json
            └── shopify.extension.toml
  5. Create a form inside the theme app extension and add a script in the assets folder that uses fetch(), to hit the online store address + the proxy prefix + proxy subpath. This will then transfer the data to your App Proxy URL, which should represent the raw IFRAME URL to your Remix App
  6. Use the relevant action() method of a new jsx/tsx route file [I created app.api.jsx], in your Remix App. The action() method gets targeted only by POST requests. Use request.json() to extract the fetch() body data.
  7. I had to remove the authenticate.admin() check, at the head of the action() method. I know this is probably very dangerous, but I couldn’t get the request to authenticate. If anyone has an answer how to get the authentication to work, I would be most grateful.

A tale of two App Proxies:

The only question, that I have is, how do I get the Theme App Extension to work with two different App Proxies?

 

Essentially, I have a development [app bridge] & production version [fly.io] of my app.

I have the same Theme App Extension, embedded in both apps.

Each app has a different App Proxy URL value.

 

When I try and send the data to my development version of the Remix App, everything works fine.

When I try and send the data to my production Remix App on fly.io, it keeps trying to send it to my development App Proxy URL, even though I have definitely set the two different App Proxy URLs, correctly.

 

Now, it seems like the Theme App Extension is agnostic with respect to the URL that is used, within the fetch() method.

 

This fetch() method param URL is:

 

online store address + the proxy prefix + proxy subpath

 

Essentially, the fetch() method URL param, is the same for both the development & production app.

It should then be able to work out which environment it is in, by referring to it's parent app. It should then be able to access the correct App Proxy URL.

It then transfers the data from the online store address URL, to the App Proxy URL.

 

But, my system seems to fail, at this transfer point.

 

Does anyone know how to resolve this?

hieunoobpro
Shopify Partner
16 0 1

i can't send data from my sign up form to back end, could you tell me how?

Charles_Roberts
Shopify Partner
47 0 9

OK. The first thing to do, is to get your App Proxy setup correctly. 

I am assuming you have created some kind of form, in your Theme App Extension. You need to make sure that you use something like fetch() or axios() to post your data to your Admin App

App Proxy setup:

 

You should point the proxy at a Remix route, so, for example:

 

app/routes folder:

 

app.api.jsx

 

Then the App Proxy URL would be:

 

https://mydomain.trycloudflare.com/app/api

 

Then your fetch() URL would be like:

 

/subpath_prefix/subpath[/route_path]


So let’s say your subpath_prefix value is apps and your subpath is app-proxy, then the form action or fetch() URL will be:

 

/apps/app-proxy/api


So what actually happens when the data is posted by a fetch() XHR request? Well, the request is intercepted by Shopify and the URL is analysed to see if it matches an App Proxy value. If there is a match, the data is forwarded to the App Proxy proxy URL value. Then, query params are added onto the end of the proxy URL.
These params include:

 

  • logged_in_customer_id
  • path_prefix
  • timestamp
  • signature


Example:

 

GET /?index=value&example.myshopify.com&logged_in_customer_id=1234567890&path_prefix=%2Fa%2Fexample&timestamp=1234567890&signature=XXXXXXXXXX

 

The signature is important. It is made up of all the other query param values, which are sorted & then hashed. The signature is created using:

 

 

createSHA256HMAC()

 

https://github.com/Shopify/shopify-api-js/blob/main/packages/shopify-api/runtime/crypto/utils.ts#L6

 

App Proxy fetch() URL:

 

Note that, you don't have to add the domain part of the URL, in the fetch() URL param.

 

Please remember that form POST requests, hit the:

 

Remix action() method

 

And GET requests, hit the:

 

Remix loader() method


If you still have problems, let me know and I will go through other potential problems, including invalid signature

 

hieunoobpro
Shopify Partner
16 0 1

I have this sign-up form in my app extension:

<div id="input-form" class="form-container">
  <form id="signupForm">
    <label for="name"><i class="fas fa-user"></i> Name:</label>
    <input type="name" id="name" name="name" required>
    <br> <!-- Xuống dòng sau label -->
    <label for="email"><i class="fas fa-envelope"></i> Email:</label>
    <input type="email" id="email" name="email" required>
    <br> <!-- Xuống dòng sau label -->
    <label for="mobile"><i class="fas fa-phone"></i> Phone:</label>
    <input type="mobile" id="mobile" name="mobile" required>
    <br> <!-- Xuống dòng sau label -->
    <label for="address"><i class="fas fa-map-marker-alt"></i> Address:</label>
    <input type="address" id="address" name="address" required>
    <br> <!-- Xuống dòng sau label -->

    <label for="gender"><i class="fas fa-venus-mars"></i> Gender:</label>
    <div class="gender-input">
      <select id="gender" name="gender" required>
        <option value="male">Male</option>
        <option value="female">Female</option>
        <option value="other">Other</option>
      </select>
    </div>
    <br>
    <div id="responseDataElement"></div>
    <br>
    <button onclick="testProxy()"><i class="fas fa-comment"></i>Let's Talk</button>
  </form>
</div>

<script>
  function testProxy() {
    const form = document.getElementById('signupForm');
    const formData = new FormData(form);
      method: 'POST',
      redirect: 'manual',
      body: JSON.stringify(Object.fromEntries(formData)),
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*',
      },
    })
      .then((response) => {
        console.log(response, 'response');
        return response.json(); // Assuming you want to parse response as JSON
      })
      .then((data) => {
        console.log(data); // Handle session data received from the backend
        const responseDataElement = document.getElementById('responseDataElement');
        responseDataElement.innerText = JSON.stringify(data);
      })
      .catch((error) => {
        console.error('Error:', error);
      });
  }
</script>
When i click the button, it send post request but it doesn't send data in submit form to my back-end code . And in my back-end code, i can't send response data to front-end
import { type ActionFunction } from '@remix-run/node';
import { Page } from '@shopify/polaris';
import type { RowDataPacket } from 'mysql2/promise';
import mysql from 'mysql2/promise';

interface Row extends RowDataPacket {
  access_token: string;
  company_token: string;
}

export const action: ActionFunction = async ({ request }) => {
  console.log("-------------------------hir app proxy--------------------------")
  console.log(request)
  console.log(request.url, "shopi")
  const baseUrl = new URL(request.url).origin;
  console.log(baseUrl);
  const connection = await mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: '',
    database: 'workchat_db'
  });
  // Execute the SQL query to fetch access_token and company_token
  const [rows] = await connection.execute<Row[]>(`
    SELECT access_token, company_token
    FROM admins
    WHERE online_store_url = ?
  `, [baseUrl]);

  // Check if any rows were returned
  if (rows.length > 0) {
    const access_token = rows[0].access_token;
    const company_token = rows[0].company_token;
    console.log(access_token, "access_token");
    console.log(company_token, "company_token");
    const dataToSend = {
      access_token: access_token,
      company_token: company_token
    };
    console.log(dataToSend, "data send")
    // Send data to the proxy endpoint
    const response = await fetch('https://minima-world.myshopify.com/apps/proxytest', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(dataToSend)
    });

    // Check if the request was successful
    if (response.ok) {
      console.log('Data sent successfully.');
    } else {
      console.error('Failed to send data:', response.statusText);
    }
  } else {
    console.log("No rows found for the given URL.");
  }
  // Close the MySQL connection
  await connection.end();
  return null;
}

const Proxy = () => {
  return <Page>Proxy</Page>
}
export default Proxy


Charles_Roberts
Shopify Partner
47 0 9

Try this:

 

fetch('/apps/proxytest', {
      method: 'POST',
      body: JSON.stringify(Object.fromEntries(formData)),
      headers: {
        'Content-Type': 'application/json
      },
    })
 
You should NOT set:
 
'Access-Control-Allow-Origin': '*',
 
It won't help you here. Anyway, this can only be set on the server. It won't really do anything when sent from the client. You can set this header in the loader()/action() method of your Remix app, if you wish to do so, but you won't need to:
 
 
Is your Remix route in the app folder?
 
Can you show me, where your Remix route is? I want to make sure that your fetch() URL param is correct?
 
Also you need to authenticate your request in your Remix app:
 
export async function action({ request, params }) {
    const { admin, session } = await authenticate.public.appProxy(request);
const formData = await request.json();
    ...
}
 
If you don't authenticate you will have trouble carrying out certain tasks. Although, in your case, I see you are not using any of the Shopify Admin Rest or GraphQl APIs, so you should be OK. However, it is good practice to authenticate, because you don't want to expose your app, to malicious activity.
 
 
hieunoobpro
Shopify Partner
16 0 1

Thank you so much, my back now has data response. but why the field response is all null? please let mr know. i have console log all data but they are all null.

<div id="input-form" class="form-container">
  <form id="signupForm">
    <label ><i class="fas fa-user"></i> Name:</label>
    <input id="name" required>
    <br> <!-- Xuống dòng sau label -->
    <label ><i class="fas fa-envelope"></i> Email:</label>
    <input id="email"  required>
    <br> <!-- Xuống dòng sau label -->
    <label ><i class="fas fa-phone"></i> Phone:</label>
    <input id="mobile" required>
    <br> <!-- Xuống dòng sau label -->
    <label ><i class="fas fa-map-marker-alt"></i> Address:</label>
    <input id="address"  required>
    <br> <!-- Xuống dòng sau label -->
    <label><i class="fas fa-venus-mars"></i> Gender:</label>
    <div class="gender-input">
      <select id="gender" required>
        <option value="male">Male</option>
        <option value="female">Female</option>
        <option value="other">Other</option>
      </select>
    </div>
    <br>
    <div id="responseDataElement"></div>
    <br>
  </form>
  <button onclick="testProxy()"><i class="fas fa-comment"></i>Let's Talk </button>
</div>

<script>

  function testProxy() {
  const form = document.getElementById('signupForm');
   const formData = new FormData(form);
 // Retrieve input values from the form
  const nameInput = document.getElementById('name').value;
    const emailInput = document.getElementById('email').value;
    const mobileInput = document.getElementById('mobile').value;
    const addressInput = document.getElementById('address').value;
    const genderSelect = document.getElementById('gender').value;

    // Log the input values to the console
    console.log('Name:', nameInput);
    console.log('Email:', emailInput);
    console.log('Phone:', mobileInput);
    console.log('Address:', addressInput);
    console.log('Gender:', genderSelect);
    fetch('/apps/proxytest', {
      method: 'POST',
      body: JSON.stringify(Object.fromEntries(formData)),
      headers: {
        'Content-Type': 'application/json',
      },
    })
      .then((response) => {
        console.log(response, 'response');
        return response.json(); // Assuming you want to parse response as JSON
      })
      .then((data) => {
        console.log(data); // Handle session data received from the backend
        const responseDataElement = document.getElementById('responseDataElement');
        responseDataElement.innerText = JSON.stringify(data);
      })
      .catch((error) => {
        console.error('Error:', error);
      });
  }
</script>
 
Charles_Roberts
Shopify Partner
47 0 9

I think you need to use the name attribute in each form field like:

 

<input type="text" name="name" value="foo" />

 

If you want to collect form data via:

const formData = new FormData(form);

 

hieunoobpro
Shopify Partner
16 0 1

i have change like you said but the data return is always default value, even when i has change string in input form. The data request to back-ens always is:
13:44:00 │ remix │ {
13:44:00 │ remix │ name: 'foo',
13:44:00 │ remix │ email: 'foo',
13:44:00 │ remix │ mobile: 'foo',
13:44:00 │ remix │ address: 'foo',
13:44:00 │ remix │ gender: 'male'
13:44:00 │ remix │ } 
this show even when i have used:

    const name = document.getElementById('name').value;
    const email = document.getElementById('email').value;
    const mobile = document.getElementById('mobile').value;
    const address = document.getElementById('address').value;
    const gender = document.getElementById('gender').value;

    // Create an object with the form data
    const formData = {
        name: name,
        email: email,
        mobile: mobile,
        address: address,
        gender: gender
    };
 
Charles_Roberts
Shopify Partner
47 0 9

Are you using:

 

const formData = await request.json();
    ...

Inside your Remix App action() method, to parse the data? 
This is very important. 

hieunoobpro
Shopify Partner
16 0 1

yes i am using it

 

Charles_Roberts
Shopify Partner
47 0 9

 

If this doesn’t work, I suggest removing:

 

const formData = new FormData(form);

 

And use something like:

 


const name = document.getElementById('name');
const email = document.getElementById('email');


const formData = JSON.stringify({
    name: name.value,
    email: email.value,
    …
});

    

I only use:

 

new FormData()

 

For file uploads. 

And finally, are you authenticating the request?

 

const { admin, session } = await authenticate.public.appProxy(request);
hieunoobpro
Shopify Partner
16 0 1

So, I have to find another way. Thank you for spending time helping me; I truly appreciate your support and guidance

Charles_Roberts
Shopify Partner
47 0 9

No worries. I hope you find a solution soon..

Charles_Roberts
Shopify Partner
47 0 9

There is one other angle, you might want to explore.

You can access Request Internals, by doing the following.

 

You might find the form data values, in here, as well:

 

export async function action({ request, params }) {

    const { admin, session } = await authenticate.public.appProxy(request);

    const formData = await request.json();

    const sym = Object.getOwnPropertySymbols(request).find(
        (s) => s.description === "Request internals"
    );

    let shop = "";
    let customerid = "0";
    let href = "";
    let signature = "";
    if(request[sym] && 'parsedURL' in request[sym] && 'href' in request[sym].parsedURL){
        href = request[sym].parsedURL.href;
    }

    if(request[sym] && 'parsedURL' in request[sym] && 'searchParams' in request[sym].parsedURL){
        const searchParam = request[sym].parsedURL.searchParams;
        shop = searchParam.get('shop');
        customerid = searchParam.get('logged_in_customer_id') || "0";
        signature = searchParam.get('signature') || "";
    }

    ...

}

 

hieunoobpro
Shopify Partner
16 0 1

Ok. i find out my problem. It seem a problem in my css file. When i remove my css it run normally. I have recieved data in back-end,. but now, how can i add data in back-end to my response 

Charles_Roberts
Shopify Partner
47 0 9
hieunoobpro
Shopify Partner
16 0 1

it's no use. In 

export const action: ActionFunction = async ({ request }) => {
i want to return an url:  return JSON.stringify(erpData2.url_wchat);
but it not showing anything in front-end
Charles_Roberts
Shopify Partner
47 0 9

You can’t return data like that. You have to use liquid

 

 

export const action = async ({request}) => {
const {storefront, liquid} = await authenticate.public.appProxy(request);

return liquid(
erpData2.url_wchat);
}

 

hieunoobpro
Shopify Partner
16 0 1

Could you help me, why after i use return like you say, it has error 404 (Not Found) when i console.log(response). Could you help me show my url to my liquid code. i want to use it for an iframe

Charles_Roberts
Shopify Partner
47 0 9

Well, the response gets sent back to the URL, that the request comes from.
Or are you saying that you want to return the response to a different URL? 

hieunoobpro
Shopify Partner
16 0 1

i mean i want to send back response but how matter way i try, it still sat response 404 when i console.log my response

hieunoobpro
Shopify Partner
16 0 1

have you send back data to front end before, could you tell me how you do that?

Nitansh
Shopify Partner
4 0 0

@Charles_Roberts @hieunoobpro 

Have you solved this. I am also stuck on the same problem.

My setup:

App proxy Subpath prefix : apps

App proxy Subpath : proxytest

 

Proxy URL: https://implications-lindsay-festival.trycloudflare.com/app/proxy

From theme app extension sending the request as : 

fetch('/apps/proxytest')
and in the remix app path is : app.proxy.jsx (app/proxy)
In the remix app I am getting the request and the session

 

 

const { admin, session } = await authenticate.public.appProxy(request);
  
  if (session) {
    console.log(session);
  }

  const response = await admin.graphql(
    `#graphql
      query productTitle {
        products(first: 1) {
          nodes {
            title
          }
        }
      }`,
  );

 

 

I am able to fetch the product also, but not getting any response back.

 

In the browser it does 307 Temporary redirect to the <store-domain>/auth/login

 

Charles_Roberts
Shopify Partner
47 0 9

You mean you are not able to send the response to the Theme App Extension page?

 

But anyway I think you need to do something like this:


const response = await admin.graphql( `#graphql query productTitle { products(first: 1) { nodes { title } } }`, );

const res = response.json();

return res;

 

It needs to return an object or an object in liquid format. But I think you maybe returning JSON. 

Remix will convert the object to JSON for you. 

Nitansh
Shopify Partner
4 0 0

yes not able to send the response to the theme app extension page

when clicking on the test button on the theme app extension page request is reaching to the remix app and I was able to call graphql query.

Nitansh_0-1714723690972.png

Nitansh_1-1714723790668.png

 

 

Nitansh
Shopify Partner
4 0 0

@Charles_Roberts any idea on this?

Charles_Roberts
Shopify Partner
47 0 9

I think your fetch() URL might be wrong. How about:

 

fetch('/apps/proxytest/proxy')