How to create and use custom APIs in Spartacus storefront? - spartacus-storefront

We have a custom API created in hybris and I need to use the data returned by that custom API in some Spartacus pages. I want to do this post login and want to call that api whenever the page refreshes.
Also I want to maintain the data in a state so that I can use it across the pages.
I know how to do it in Angular but I am confused how to do it in Spartacus. Can someone please help me

You should treat spartacus as external lib. If you know how implement it in Angular, just do it. Most of our actions like login are exported in public API:
import { ActivatedRouterStateSnapshot, AuthActions } from '#spartacus/core';
import { RouterNavigatedAction, ROUTER_NAVIGATED } from '#ngrx/router-store';
#Injectable()
export class YourEffects {
#Effect()
yourActionOnLogin$: Observable<YourActions.XXX> = this.actions$.pipe(
ofType(AuthActions.LOGIN),
map(() => new CheckoutActions.XXX())
);
#Effect()
yourActionOnNavigation$: Observable<YourActions.YYY> = this.actions$.pipe(
ofType<RouterNavigatedAction<ActivatedRouterStateSnapshot>>(
ROUTER_NAVIGATED
),
map(() => new YourActions.YYY())
);
}
You can create and provide own modules for part of ngrx store and occ adapters (API) as well.

Related

404 error calling GET API from a shopify theme-extension

I'm new to shopify development, and can't figure out how to call an authenticated API from a shopify theme-extension. Essentially, I'm trying to make a theme extension, where one of the functionalities is that when a checkbox is clicked, an API that counts the number of products is called.
I have a working api that gets the product count, and in web>index.js, I have set-up the end-point:
app.get("/api/products/count", async (_req, res) => {
const countData = await shopify.api.rest.Product.count({
session: res.locals.shopify.session,
});
res.status(200).send(countData);
});
Under web>frontend>hooks, I have the authenticated hooks set-up as shown below. I've tested that if I call the "api/products/count" API from one of the web pages using useAppQuery, it works as expected, and returns the product count.
import { useAuthenticatedFetch } from "./useAuthenticatedFetch";
import { useMemo } from "react";
import { useQuery } from "react-query";
export const useAppQuery = ({ url, fetchInit = {}, reactQueryOptions }) => {
const authenticatedFetch = useAuthenticatedFetch();
const fetch = useMemo(() => {
return async () => {
const response = await authenticatedFetch(url, fetchInit);
return response.json();
};
}, [url, JSON.stringify(fetchInit)]);
return useQuery(url, fetch, {
...reactQueryOptions,
refetchOnWindowFocus: false,
});
};
In my theme extension code, I've added an event listener to the checkbox which calls getProductCount. In getProductCount, I want to call /api/products/count:
import { useAppQuery } from "../../../web/frontend/hooks";
export const getProductCount = (product) => {
const {
data,
refetch: refetchProductCount,
isLoading: isLoadingCount,
isRefetching: isRefetchingCount,
} = useAppQuery({
url: "/api/products/count",
reactQueryOptions: {
onSuccess: () => {
setIsLoading(false);
},
},
});
}
However, when I run locally and click the checkbox, it returns a 404 error trying to find useAppQuery. The request URL is https://cdn.shopify.com/web/frontend/hooks. It seems like the authentication isn't working because that URL looks incorrect.
Am I missing a step that I need to do in order to call an authenticated API from a theme-extension?
I thought the issue was just the import path for useAppQuery but I've tried different paths, and they all return the same 404 issue.
If you want a hot tip here. In your theme App extension, you do not actually need to make an API call to get a product count. In your theme app extension, you can just use Liquid, and dump the product count out to a variable of your choice, and use the count, display the count, do whatever.
{{ shop.product_count }}
Of course, this does not help you if you need other storefront API calls in your theme App extension, but whatever. In my experience, I render the API Access Token I need in my theme app extension, and then making my Storefront API calls is just a fetch().
The only time I would use authenticated fetch, is when I am doing embedded App API calls, but that is a different beast from a theme app extension. In there, you do not get to make authenticated calls as the front-end is verboten for those of course. Instead you'd use App Proxy for security.
TL:DR; Storefront API calls with a token should not fail with a 404 if you call the right endpoint. You can use Storefront API inside a theme app extension. Inside a theme app extension, if you need backend Admin API access, you can use App Proxy calls.

Spartacus Configurable Product Integration: Use custom Form Elements

I am currently experiencing issues with the Configurable Product Integration of Spartacus. So far I have set up the storefront to show my configurable products, which is working quite well.
Now I am trying to customize a form element, let's say an input field to insert a custom suffix that is dynamically added by the backend. Specifically, I am trying to replace the cx-configurator-attribute-input-fieldinside https://github.com/SAP/spartacus/blob/develop/feature-libs/product-configurator/rulebased/components/form/configurator-form.component.html
So far I have tried to import it in the module:
ConfigModule.withConfig({
cmsComponents: {
ConfiguratorAttributeInputField: {
component:CustomConfiguratorComponent
}
}
})
which is not working, probably because the component is not a static CmsComponent.
I also tried importing it via outlet definition in typescript:
export class CustomConfiguratorComponent implements OnInit{
constructor(private componentFactoryResolver: ComponentFactoryResolver, private outletService: OutletService){}
ngOnInit(){
const factory = this.componentFactoryResolver.resolveComponentFactory(CustomInputFieldComponent);
this.outletService.add('cx-configurator-attribute-input-field', factory);
console.log("REGISTERED");
}
}
which is also not working.
When I add the outlet via ng-template to a template reference, I can see the component, so the import of the component should be working correctly:
<ng-template cxOutletRef="VariantConfigurationTemplate" cxOutletPos="before">
<app-custom-input-field></app-custom-input-field>
</ng-template>
How can I get this to work, so that I can replace a dynamically added Spartacus component? Specifically the ConfiguratorAttributeInputFieldComponent (https://github.com/SAP/spartacus/blob/develop/feature-libs/product-configurator/rulebased/components/attribute/types/input-field/configurator-attribute-input-field.component.ts)

How to acced to google object in vue 2?

I'm trying to use the google maps API at my vue2 project and I have tried some ways that have failed. After using the vue2googlemaps module and the node module from google I have decided to use the CDN directly and add it to the index page. My problem now is that to acced to the google object, for example, to create a Marker or something like that, I need to use this.marker = new window.google.maps.Marker() for example, but in the tutorials I have seen, everyone uses directly the google object and never uses that window. I can`t understand why it happens. It would be appreciated if someone shows me the correct way to import or use this library on google.
It's because your template's code is compiled and executed in your component instance (a.k.a vm) 's scope, not in the global (a.k.a. window) scope.
To use google directly in your template you could add the following computed:
computed: {
google: () => window.google
}
If your problem is not having google defined in the component's <script>, a simple solution is to add it as a const at the top:
import Vue from 'vue';
const google = window.google;
export default Vue.extend({
computed: {
google: () => google // also make it available in template, as `google`
}
})
An even more elegant solution is to teach webpack to get google from the window object whenever it's imported in any of your components:
vue.config.js:
module.exports = {
configureWebpack: {
externals: {
google: 'window.google'
}
}
}
This creates a google namespace in your webpack configuration so you can import from it in any of your components:
import google from 'google';
//...
computed: {
google: () => google // provide it to template, as `google`
}
Why do I say it's more elegant?
Because it decouples the component from the context and now you don't need to modify the component when used in different contexts (i.e: in a testing environment, which might not even use a browser, so it might not have a window object, but a global instead; all you'd have to do in this case is to define a google namespace in that environment and that's where your component will get its google object from; but you wouldn't have to tweak or mock any of the component's methods/properties).

Access data model in VueJS with Cypress (application actions)

I recently came across this blog post: Stop using Page Objects and Start using App Actions. It describes an approach where the application exposes its model so that Cypress can access it in order to setup certain states for testing.
Example code from the link:
// app.jsx code
var model = new app.TodoModel('react-todos');
if (window.Cypress) {
window.model = model
}
I'd like to try this approach in my VueJS application but I'm struggling with how to expose "the model".
I'm aware that it's possible to expose the Vuex store as described here: Exposing vuex store to Cypress but I'd need access to the component's data().
So, how could I expose e.g. HelloWorld.data.message for being accessible from Cypress?
Demo application on codesandbox.io
Would it be possible via Options/Data API?
Vue is pretty good at providing it's internals for plugins, etc. Just console.log() to discover where the data sits at runtime.
For example, to read internal Vue data,
either from the app level (main.js)
const Vue = new Vue({...
if (window.Cypress) {
window.Vue = Vue;
}
then in the test
cy.window().then(win => {
const message = win.Vue.$children[0].$children[0].message;
}
or from the component level
mounted() {
if (window.Cypress) {
window.HelloWorld = this;
}
}
then in the test
cy.window().then(win => {
const message = win.HelloWorld.message;
}
But actions in the referenced article implies setting data, and in Vue that means you should use Vue.set() to maintain observability.
Since Vue is exposed on this.$root,
cy.window().then(win => {
const component = win.HelloWorld;
const Vue = component.$root;
Vue.$set(component, 'message', newValue);
}
P.S. The need to use Vue.set() may go away in v3, since they are implementing observability via proxies - you may just be able to assign the value.
Experimental App Action for Vue HelloWorld component.
You could expose a setter within the Vue component in the mounted hook
mounted() {
this.$root.setHelloWorldMessage = this.setMessage;
},
methods: {
setMessage: function (newValue) {
this.message = newValue;
}
}
But now we are looking at a situation where the Cypress test is looking like another component of the app that needs access to state of the HelloWorld.
In this case the Vuex approach you referenced seems the cleaner way to handle things.

Shopify - Get shop domain inside a app

I'm new to Shopify app developing and I'm using Node,Express for the back-end and react with polaris libaray.
My question is how to get the shop's domain the request is initiating throug h the app. When I searched I could only found one used in Ruby ShopifyAPI::Shop.current and I'm looking for the similar thing to use in node?
For examples check out https://github.com/BKnights/kotn-shopify-utils
Yes it uses a session.
The code is pretty idiosyncratic. I published it mostly as an easy way to share between my own projects but it's worked pretty well for those.
If you use this where you may scale your servers you'd need to replace the session engine with something more distributed. Cookie sessions work.
This expects to route the setup page of the app to /preferences. Look at that route with the validSession, session, middleware
For passing the domain to Polaris I put the shop info into a plain JS object on the containing page (it's a dustjs template):
<script type="text/javascript">
var KotN = {
shop:'{shop}',
apiKey: '{apiKey}',
shopOrigin: 'https://{shop}.myshopify.com',
locale:'{locale}' || (navigator.languages ? (navigator.language || navigator.languages[0]) : (navigator.userLanguage || navigator.browerLanguage))
};
</script>
and then the Polaris component looks like:
import * as React from 'react';
import {EmbeddedApp} from '#shopify/polaris/embedded';
import ShopProvider from './stores/ShopProvider';
import Status from './views/status';
const shop = KotN.shop;
const shopOrigin = KotN.shopOrigin;
const apiKey = KotN.apiKey;
console.log('shop: '+ shop +' and origin: '+ shopOrigin);
export default class MyApp extends React.Component {
render() {
return (
<EmbeddedApp
apiKey={apiKey}
shopOrigin={shopOrigin}
forceRedirect={true}
debug={true}
>
<ShopProvider>
<Status />
</ShopProvider>
</EmbeddedApp>
);
}
}
I know this is a old thread but i've stumbled here while i was searching for the answer and just wanted to share my solution with you guys that also have the same problem. This is how i'm getting shop domain.
let shopDomain = new URL(window.location).searchParams.get("shop");