How to tell if a Shopify store is using Online Store 2.0 theme? - shopify

I'm building a Shopify app
I want to know if a majority of potential clients are using Online Store 2.0 themes or not.
Is there a way to tell this by looking only at their websites? (i.e. checking if some script is loaded in network tab that only loads for online store 2.0 themes)

On the Shopify Product reviews sample app, they have this endpoint, which returns whether the current theme supports app blocks (only 2.0 themes support app blocks)
/**
* This REST endpoint is resposible for returning whether the store's current main theme supports app blocks.
*/
router.get(
"/api/store/themes/main",
verifyRequest({ authRoute: "/online/auth" }),
async (ctx) => {
const session = await Shopify.Utils.loadCurrentSession(ctx.req, ctx.res);
const clients = {
rest: new Shopify.Clients.Rest(session.shop, session.accessToken),
graphQL: createClient(session.shop, session.accessToken),
};
// Check if App Blocks are supported
// -----------------------------------
// Specify the name of the template we want our app to integrate with
const APP_BLOCK_TEMPLATES = ["product"];
// Use `client.get` to request list of themes on store
const {
body: { themes },
} = await clients.rest.get({
path: "themes",
});
// Find the published theme
const publishedTheme = themes.find((theme) => theme.role === "main");
// Get list of assets contained within the published theme
const {
body: { assets },
} = await clients.rest.get({
path: `themes/${publishedTheme.id}/assets`,
});
// Check if template JSON files exist for the template specified in APP_BLOCK_TEMPLATES
const templateJSONFiles = assets.filter((file) => {
return APP_BLOCK_TEMPLATES.some(
(template) => file.key === `templates/${template}.json`
);
});
// Get bodies of template JSONs
const templateJSONAssetContents = await Promise.all(
templateJSONFiles.map(async (file) => {
const {
body: { asset },
} = await clients.rest.get({
path: `themes/${publishedTheme.id}/assets`,
query: { "asset[key]": file.key },
});
return asset;
})
);
// Find what section is set as 'main' for each template JSON's body
const templateMainSections = templateJSONAssetContents
.map((asset, index) => {
const json = JSON.parse(asset.value);
const main = json.sections.main && json.sections.main.type;
return assets.find((file) => file.key === `sections/${main}.liquid`);
})
.filter((value) => value);
// Request the content of each section and check if it has a schema that contains a
// block of type '#app'
const sectionsWithAppBlock = (
await Promise.all(
templateMainSections.map(async (file, index) => {
let acceptsAppBlock = false;
const {
body: { asset },
} = await clients.rest.get({
path: `themes/${publishedTheme.id}/assets`,
query: { "asset[key]": file.key },
});
const match = asset.value.match(
/\{\%\s+schema\s+\%\}([\s\S]*?)\{\%\s+endschema\s+\%\}/m
);
const schema = JSON.parse(match[1]);
if (schema && schema.blocks) {
acceptsAppBlock = schema.blocks.some((b) => b.type === "#app");
}
return acceptsAppBlock ? file : null;
})
)
).filter((value) => value);
/**
* Fetch one published product that's later used to build the editor preview url
*/
const product = await getFirstPublishedProduct(clients.graphQL);
const editorUrl = `https://${session.shop}/admin/themes/${
publishedTheme.id
}/editor?previewPath=${encodeURIComponent(
`/products/${product?.handle}`
)}`;
/**
* This is where we check if the theme supports apps blocks.
* To do so, we check if the main-product section supports blocks of type #app
*/
const supportsSe = templateJSONFiles.length > 0;
const supportsAppBlocks = supportsSe && sectionsWithAppBlock.length > 0;
ctx.body = {
theme: publishedTheme,
supportsSe,
supportsAppBlocks,
/**
* Check if each of the sample app's app blocks have been added to the product.json template
*/
containsAverageRatingAppBlock: containsAppBlock(
templateJSONAssetContents[0]?.value,
"average-rating",
process.env.THEME_APP_EXTENSION_UUID
),
containsProductReviewsAppBlock: containsAppBlock(
templateJSONAssetContents[0]?.value,
"product-reviews",
process.env.THEME_APP_EXTENSION_UUID
),
editorUrl,
};
ctx.res.statusCode = 200;
}
);

Related

React-admin image upload data provider showing undefined

I'm trying to use the customDataProvider example provided by react-admin's documentation
//dataProvider.js
import simpleRestProvider from 'ra-data-simple-rest'
const dataProvider = simpleRestProvider(process.env.REACT_APP_API);
export const myDataProvider = {
...dataProvider,
update: (resource, params) => {
if (resource !== 'recipes') {
// fallback to the default implementation
return dataProvider.update(resource, params);
}
/**
* For posts update only, convert uploaded image in base 64 and attach it to
* the `picture` sent property, with `src` and `title` attributes.
*/
// Freshly dropped pictures are File objects and must be converted to base64 strings
const newPictures = params.data.pictures.filter(
p => p.rawFile instanceof File
);
const formerPictures = params.data.pictures.filter(
p => !(p.rawFile instanceof File)
);
return Promise.all(newPictures.map(convertFileToBase64))
.then(base64Pictures =>
base64Pictures.map(picture64 => ({
src: picture64,
title: `${params.data.title}`,
}))
)
.then(transformedNewPictures =>
dataProvider.update(resource, {
data: {
...params.data,
pictures: [
...transformedNewPictures,
...formerPictures,
],
},
})
);
},
};
/**
* Convert a `File` object returned by the upload input into a base 64 string.
* That's not the most optimized way to store images in production, but it's
* enough to illustrate the idea of data provider decoration.
*/
const convertFileToBase64 = file =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(file.rawFile);
});
With this in the page / component
//recipe.js snippet
<ImageInput source="pictures" accept="image/*" multiple="true">
<ImageField source="src" title="title"/>
</ImageInput>
but the image title is showing as undefined in the PUT request body
I have also lost the ID of the item from the URL, this also shows as undefined in the PUT URL
Where am I going wrong?

How to get state in Nuxt js with composition api?

setup(){
const columns = computed(()=>store.state['subCategory'].subCategoryColumnsData[subCategoryName.value]);
const { fetch } = useFetch(async () => {
await store.dispatch('subCategory/getColumnsQuery', {
categories: subCategoryId.value,
page: 1,
subCategoryName: subCategoryName.value,
})
});
fetch();
}
I want to switch between pages in my project. Whenever I switched another page, I send request to get data with latest updates. This code works well for the first time when page was loaded, but it doesn't work when I switched from one page to another page. But if I check store state, I can see it in store. If I visit same page second time , I can see data this time.
But if I change my code like this, it works well. I did not get why it does not work true in the first sample
setup(){
const columns = ref([])
const { fetch } = useFetch(async () => {
await store.dispatch('subCategory/getColumnsQuery', {
categories: subCategoryId.value,
page: 1,
subCategoryName: subCategoryName.value,
})
}).then(() => (columns.value = store.state['subCategory'].subCategoryColumnsData[subCategoryName.value]));
fetch();
}
Can you test it? sample:
const state = reactive({ columns: computed(() => yourstore })
// do not need to call fetch because this hook is a function
useFetch(async () => { await store.dispatch(url) })
return {
...toRefs(state),
}

Ant Design Pro dynamic menu not showing up

Following the instruction at https://pro.ant.design/docs/router-and-nav#fetch-menu-from-server
I changed file BasicLayout.tsx as below. Menu is not showing up.
...
const testMenu = [{name:"login", path:"/user/login"}] as any;
const [menuData, setMenuData] = useState([]);
useEffect(() => {
if (dispatch) {
dispatch({
type: 'user/fetchCurrent',
});
}
setMenuData(testMenu)
}, []);
...
menuDataRender={()=>menuData}
...
I was doing same as you and failed as you. Document is still wrong.
And I found getting menu from server has a lot of bug with ant design pro v4. (Maybe I did not know)
So my final decision is to display all menu from /config/config.ts as designed initially.
And get only authorization information from server and set only authority to show only logged in user related menu.
So my solution (not correct answer) is:
I referenced this link. https://umijs.org/docs/runtime-config#patchroutes-routes-
Created file /src/app.tsx and inserted code as follow:
interface PathAndIdsModel {
path: string;
ids: string[];
}
const setAuthority = (routes: any, pathAndIds: PathAndIdsModel[]) => {
routes.forEach((route: any) => {
const found = pathAndIds.find((item) => item.path === route.path);
if (found) {
// eslint-disable-next-line no-param-reassign
route.authority = [...new Set([...(route.authority || []), ...found.ids])];
}
if (route.routes) {
setAuthority(route.routes, pathAndIds);
}
});
};
async function patchRoutes({ routes }) {
const response = await fetch(`https://localhost:44357/authorities`);
const pathAndIds = await response.json();
setAuthority(routes, pathAndIds);
}
export { patchRoutes };
Inserted following code to ASP.Net Core Controller:
[HttpGet("/authorities")]
public IEnumerable<object> Authorities()
{
return new[]
{
new {
Path = "/dashboard/analysis",
Ids = new [] { "user", "admin", },
},
new {
Path = "/dashboard/monitor",
Ids = new [] { "user", "admin", },
},
new {
Path = "/dashboard/workplace",
Ids = new [] { "admin", },
},
new {
Path = "/form/basic-form",
Ids = new [] { "admin", },
},
};
}
/dashboard/workplace and /form/basic-form page will be hidden if logged in as user, but shows if logged in as admin.
I tried to get full routes from server, but failed because of async call, UmiJS did not wait until fetching from server and setting new routes.
So when I fetched routes from server and changed routes, UmiJS already converted icon and component of old routes and my new routes never changed.

React-Admin <ImageInput> to upload images to rails api

I am trying to upload images from react-admin to rails api backend using active storage.
In the documentation of react-admin it says: "Note that the image upload returns a File object. It is your responsibility to handle it depending on your API behavior. You can for instance encode it in base64, or send it as a multi-part form data" I am trying to send it as a multi-part form.
I have been reading here and there but I can not find what I want, at least a roadmap of how I should proceed.
You can actually find an example in the dataProvider section of the documentation.
You have to decorate your dataProvider to enable the data upload. Here is the example of transforming the images into base64 strings before posting the resource:
// in addUploadFeature.js
/**
* Convert a `File` object returned by the upload input into a base 64 string.
* That's not the most optimized way to store images in production, but it's
* enough to illustrate the idea of data provider decoration.
*/
const convertFileToBase64 = file => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file.rawFile);
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
});
/**
* For posts update only, convert uploaded image in base 64 and attach it to
* the `picture` sent property, with `src` and `title` attributes.
*/
const addUploadFeature = requestHandler => (type, resource, params) => {
if (type === 'UPDATE' && resource === 'posts') {
// notice that following condition can be true only when `<ImageInput source="pictures" />` component has parameter `multiple={true}`
// if parameter `multiple` is false, then data.pictures is not an array, but single object
if (params.data.pictures && params.data.pictures.length) {
// only freshly dropped pictures are instance of File
const formerPictures = params.data.pictures.filter(p => !(p.rawFile instanceof File));
const newPictures = params.data.pictures.filter(p => p.rawFile instanceof File);
return Promise.all(newPictures.map(convertFileToBase64))
.then(base64Pictures => base64Pictures.map((picture64, index) => ({
src: picture64,
title: `${newPictures[index].title}`,
})))
.then(transformedNewPictures => requestHandler(type, resource, {
...params,
data: {
...params.data,
pictures: [...transformedNewPictures, ...formerPictures],
},
}));
}
}
// for other request types and resources, fall back to the default request handler
return requestHandler(type, resource, params);
};
export default addUploadFeature;
You can then apply this on your dataProvider:
// in dataProvider.js
import simpleRestProvider from 'ra-data-simple-rest';
import addUploadFeature from './addUploadFeature';
const dataProvider = simpleRestProvider('http://path.to.my.api/');
const uploadCapableDataProvider = addUploadFeature(dataProvider);
export default uploadCapableDataProvider;
Finally, you can use it in your admin as usual:
// in App.js
import { Admin, Resource } from 'react-admin';
import dataProvider from './dataProvider';
import PostList from './posts/PostList';
const App = () => (
<Admin dataProvider={uploadCapableDataProvider}>
<Resource name="posts" list={PostList} />
</Admin>
);
When using files, use a multi-part form in the react front-end and for example multer in your API backend.
In react-admin you should create a custom dataProvider and extend either the default or built a custom one. Per implementation you should handle the file/files upload. For uploading a file or files from your custom dataprovider in react-admin:
// dataProvider.js
// this is only the implementation for a create
case "CREATE":
const formData = new FormData();
for ( const param in params.data ) {
// 1 file
if (param === 'file') {
formData.append('file', params.data[param].rawFile);
continue
}
// when using multiple files
if (param === 'files') {
params.data[param].forEach(file => {
formData.append('files', file.rawFile);
});
continue
}
formData.append(param, params.data[param]);
}
return httpClient(`myendpoint.com/upload`, {
method: "POST",
body: formData,
}).then(({ json }) => ({ data: json });
From there you pick it up in your API using multer, that supports multi-part forms out-of-the-box. When using nestjs that could look like:
import {
Controller,
Post,
Header,
UseInterceptors,
UploadedFile,
} from "#nestjs/common";
import { FileInterceptor } from '#nestjs/platform-express'
#Controller("upload")
export class UploadController {
#Post()
#Header("Content-Type", "application/json")
// multer extracts file from the request body
#UseInterceptors(FileInterceptor('file'))
async uploadFile(
#UploadedFile() file : Record<any, any>
) {
console.log({ file })
}
}

Deleting documents in Firestore not updating onSnapshot

I am fetching a list of items from firestore in react native.
If an item document is updated, my list refreshes as expected through onSnapshot. But if the document is deleted, my list does not refresh.
Is there a way for me to catch deleted documents?
this.unsubscribe = firebase.firestore().collection('bigItems').doc(id)
.collection('littleItems').onSnapshot(this.getItems)
getItems = (querySnapshot) => {
const items = [];
querySnapshot.forEach((doc) => {
firebase.firestore().collection('events').doc(doc.id).get()
.then(item => {
const { id } = item.data();
items.push({
id: item.id
});
this.setState({
items: items,
loading: false,
});
})
.catch(function (err) {
return err;
});
})
}
Try if the following approach works for you:
const dataRef = firebase.firestore()
.collection('bigItems').doc(id)
.collection('littleItems').onSnapshot(res => {
// Listen to change type here
res.docChanges().forEach(change => {
const docName=change.doc.id;
const docData=change.doc.data();
// Refer to any field in the document like:
// E.g. : data.name, data.text etc.
// i.e. (whatever fields you used for the document)
if (change.type === "added") {
// Append to screen
}
if (change.type === "removed") {
// Now, Manually remove from UI
// Data from state before snapshot is still here,
// use some reference in data to remove from screen.
}
});
});