Using Shopify API inside Tapcart custom block? - shopify

I am exploring some updates to the product page on my Shopify Tapcart mobile app. I will have to make use of Custom blocks and the Shopify API to retrieve a list of products and their tags. Unfortunately, I have read the Shopify ajax documentation which says:
You can't use the Ajax API on a Shopify custom storefront.
As Tapcart does count as a custom storefront, is there an alternative method to retrieve a list of products on my Shopify site?

Yes you can do this, here's a boilerplate example:
(You can go here for other examples too - https://github.com/Tapcart-Templates)
const STOREFRONT_ACCESS_TOKEN = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
const GRAPHQL_URL = "https://tapcart-boutique.myshopify.com/api/2022-10/graphql.json";
const productIds = `["gid://shopify/Product/7444358463640"]`;
const productQuery = () => `
query {
nodes(ids: ${productIds}) {
... on Product {
id
title
handle
tags
}
}
}
`;
const GRAPHQL_BODY = () => {
return {
async: true,
crossDomain: true,
method: "POST",
headers: {
"X-Shopify-Storefront-Access-Token": STOREFRONT_ACCESS_TOKEN,
"Content-Type": "application/graphql",
},
body: productQuery(),
};
};
fetch(GRAPHQL_URL, GRAPHQL_BODY())
.then((res) => res.json())
.then((productResponse) => {
console.log(productResponse);
});
When you've retrieved all the needed product IDs from Shopify, you can use the openProduct app action to switch to different PDPs. Make sure to use the isRelatedProduct boolean (set it to true) to ensure the transition is smooth as butter.
https://docs.tapcart.com/docs/app-actions

Related

How can I access the react-admin store in the dataprovider? [React Admin 4.x]

I'm trying to make use of a user-set variable in the react admin store when making API calls.
Specifically, I am storing a workspace ID in the store, which the user can set through a switcher. I want to be able to access this ID when making API calls, so that I can send over the workspace ID as a url parameter in the API request.
One soluion is to try to get the data directly from localstorage, but that seems hacky. Is there a btter way?
I'm using the latest version of react admin
In recent versions of React-admin, pass parameters like this:
"React-admin v4 introduced the meta option in dataProvider queries, letting you add extra parameters to any API call.
Starting with react-admin v4.2, you can now specify a custom meta in <List>, <Show>, <Edit> and <Create> components,
via the queryOptions and mutationOptions props."
https://marmelab.com/blog/2022/09/05/react-admin-septempter-2022-updates.html#set-query-code-classlanguage-textmetacode-in-page-components
import { Edit, SimpleForm } from 'react-admin'
const PostEdit = () => (
<Edit mutationOptions={{ meta: { foo: 'bar' } }}>
<SimpleForm>...</SimpleForm>
</Edit>
)
#MaxAlex approach works well, but I went with a localStorage route, by setting a header in the fetchHTTP client defined with the dataprovider. This way I didn't have to modify each and every route.
const httpClient = (url: any, options: any) => {
if (!options) {
options = {};
}
if (!options.headers) {
options.headers = new Headers({ Accept: 'application/json' });
}
const token = inMemoryJWT.getToken();
const organization = localStorage.getItem('RaStore.organization');
if (token) {
options.headers.set('Authorization', `Bearer ${token}`);
} else {
inMemoryJWT.getRefreshedToken().then((gotFreshToken) => {
if (gotFreshToken) {
options.headers.set(
'Authorization',
`Bearer ${inMemoryJWT.getToken()}`,
);
}
});
}
if (organization) {
options.headers.set('Organization-ID', organization);
}
return fetchUtils.fetchJson(url, options);
};
I also looked into the react admin internals, and the store provider is one level below the dataprovider. This means there isn't an easy way to access the store without refactoring the entire Admin provider stack.

Is there a way to substitute dynamic base url in RTK-Query with React Native?

I found one way. I can store base url in AsyncStorage so that the user can reload the page and still have access to that url. But there is one problem. I can’t have asynchronous code inside RTK endpoints.
const postAuthEndpoint = api.injectEndpoints({
endpoints: build => ({
postAuth: build.mutation<PostAuthResponse, PostAuthRequest>({
query: async data => {
// throws an error:
// Type 'Promise<{ url: string; method: string; body: PostAuthRequest; }>'
// is not assignable to type 'string | FetchArgs'
const baseUrl = await AsyncStorage.getItem('baseUrl');
return {
url: `${baseUrl}/auth`,
method: 'POST',
body: data,
};
},
}),
}),
});
Because of this, I decided to create a custom hook, that performs an async operation to get the base url. Then the custom hook passes the base url to the api hook from RTK-Query, which we pass to the custom hook. And returns wrapped mutation with the rest of parameters.
export const useEndpointWrapper = (endpoint: any) => {
const [mutation, ...rest] = endpoint;
const wrappedMutation = async (args: Request) => {
const baseUrl = await AsyncStorage.getItem('baseUrl');
return mutation({ baseUrl, ...args }).unwrap();
};
return [wrappedMutation, rest];
};
The main disadvantage here is that the TypeScript typing breaks down. This is solvable, but inconvenient.
Maybe there are some other ways to substitute the dynamic base url in react native?

Is there any way I can access Admin API(GraphQL) in theme.liquid file using JavaScript(<script>...<script>)?

I am trying to fetch products by 'SKU', which is only possible using Admin API. I need to inject a JavaScript code snippet into theme.liquid file. Can I achieve this via JavaScript only? So far my code looks something like this:
<script>
const query = `{
productVariants(first: 1, query: "sku:<SKU>") {
edges {
node {
id
price
product {
title
description
featuredImage {
id
originalSrc
}
}
}
}
}
}`;
const STOREFRONT_ACCESS_TOKEN = 'xxxxxxxxxx';
const GRAPHQL_URL = 'https://<my-store>.myshopify.com/admin/api/2021-01/graphql.json';
const GRAPHQL_BODY = {
'method': 'POST',
'headers': {
'X-Shopify-Storefront-Access-Token': STOREFRONT_ACCESS_TOKEN,
'Content-Type': 'application/json',
},
'body': JSON.stringify({ query })
}
fetch(GRAPHQL_URL, GRAPHQL_BODY)
.then(res => res.json())
.then(data => {
console.log(data);
})
.catch((error) => {
console.log(error);
});
</script>
I am not very well familiar with Shopify and Shopify's APIs(Storefront, Admin). I tried every possible way but reached dead end. I would really appreciate if someone can redirect me to right resources. Thank you!
Your code looks loosely like the code from the docs here: https://shopify.dev/tutorials/authenticate-with-oauth
Two issues, really:
in this line:
'X-Shopify-Storefront-Access-Token': STOREFRONT_ACCESS_TOKEN,
you need be using a token which you get after you request it from
https://{shop}.myshopify.com/admin/oauth/access_token
the bigger issue, though, is:
to do so only through the theme code, you would ultimately have to expose your secret keys via the front end code, which is going to be a security risk. The technically correct way to use the Admin API would either be to set up a server that runs an embedded app and store those keys in a .env file there.

Why is it considered poor practice to use Axios or HTTP calls in components?

In this article, it says:
While it’s generally poor practice, you can use Axios directly in your components to fetch data from a method, lifecycle hook, or whenever.
I am wondering why? I usually use lifecycle hooks a lot to fetch data (especially from created()). Where should we write the request calls?
Writing API methods directly in components increases code lines and make difficult to read.
As far as I believe the author is suggesting to separate API methods into a Service.
Let's take a case where you have to fetch top posts and operate on data. If you do that in component it is not re-usable, you have to duplicate it in other components where ever you want to use it.
export default {
data: () => ({
top: [],
errors: []
}),
// Fetches posts when the component is created.
created() {
axios.get(`http://jsonplaceholder.typicode.com/posts/top`)
.then(response => {
// flattening the response
this.top = response.data.map(item => {
title: item.title,
timestamp: item.timestamp,
author: item.author
})
})
.catch(e => {
this.errors.push(e)
})
}
}
So when you need to fetch top post in another component you have to duplicate the code.
Now let's put API methods in a Service.
api.js file
const fetchTopPosts = function() {
return axios.get(`http://jsonplaceholder.typicode.com/posts/top`)
.then(response => {
// flattening the response
this.top = response.data.map(item => {
title: item.title,
timestamp: item.timestamp,
author: item.author
})
}) // you can also make a chain.
}
export default {
fetchTopPosts: fetchTopPosts
}
So you use the above API methods in any components you wish.
After this:
import API from 'path_to_api.js_file'
export default {
data: () => ({
top: [],
errors: []
}),
// Fetches posts when the component is created.
created() {
API.fetchTopPosts().then(top => {
this.top = top
})
.catch(e => {
this.errors.push(e)
})
}
}
It's fine for small apps or widgets, but in a real SPA, it's better to abstract away your API into its own module, and if you use vuex, to use actions to call that api module.
Your component should not be concerned with how and from where its data is coming. The component is responsible for UI, not AJAX.
import api from './api.js'
created() {
api.getUsers().then( users => {
this.users = users
})
}
// vs.
created() {
axios.get('/users').then({ data }=> {
this.users = data
})
}
In the above example, your "axios-free" code is not really much shorter, but imagine what you could potentially keep out of the component:
handling HTTP errors, e.g. retrying
pre-formatting data from the server so it fits your component
header configuration (content-type, access token ...)
creating FormData for POSTing e.g. image files
the list can get long. all of that doesn't belong into the component because it has nothing to do with the view. The view only needs the resulting data or error message.
It also means that you can test your components and api independently.

Click on link changes the url but not the content/data on page

the story:
I am on product page #/product/7 and on the same page I have 4 more products that are similar to the one that is being viewed. All these products have links to their pages:
router-link(:to="{ name: 'product', params: { id: product.id }}" v-text='product.title').
the problem:
When I click on any of the product links, the url changes but the content remains the same. So, if I am on #/product/7 and click on #/product/8 the url only will change. If I navigate from /product/:id page and click on a product it takes me to the right page with proper content.
As you can see on screenshot, current product id is 15, but the content is the one from the id 7, as shown in url at the bottom while I was hovering over the Sleek Silk Shirt product in cart.
Any ideas how to fix this?
You have to update the data of products variable when you change the route as vue optimises page reloads and does not reload in your case if you are on same route.
You can adapt the approach: Fetching Before Navigation described in vue-router docs:
With this approach we fetch the data before actually navigating to the new route. We can perform the data fetching in the beforeRouteEnter guard in the incoming component, and only call next when the fetch is complete:
export default {
data () {
return {
product: {},
error: null
}
},
beforeRouteEnter (to, from, next) {
getProduct(to.params.id, (err, product) => {
if (err) {
// display some global error message
next(false)
} else {
next(vm => {
vm.product = product
})
}
})
},
// when route changes and this component is already rendered,
// the logic will be slightly different.
watch: {
$route () {
this.product = {}
getProduct(this.$route.params.id, (err, product) => {
if (err) {
this.error = err.toString()
} else {
this.product = product
}
})
}
}
}
I couldnt really internalise the above answer with 'getProduct', so to be put simply.
I am using a Store and I needed to watch the $route and when it changes I called my store to dispatch the api call.
watch: {
$route () {
this.$store.dispatch('fetchStudyStandards',
this.$route.params.standardID);
}
}
So basically watch the route and if it changes, re do your api call.