Can I run `stencil push` command without prompt? - bitbucket
I'm trying to configure bitbucket pipelines with bigcommerce stencil.
The problem is the stencil push command asks some questions. I would like to auto-respond those questions.
Is that possible?
These are the questions prompted:
* Would you like to apply your theme to your store at http://xxxxxxx/? (y/N)
* Which variation would you like to apply?
- Light
- Bold
- Warm
You will need to make changes to the existing stencil-cli to make this work.
Stencil-cli uses the Commander package. My solution was to create an additional flag that would skip all the prompts if you supplied a variant name. This was created from stencil-cli version 1.13.1 so you may need to modify the example.
Inside /bin/stencil-push:
#!/usr/bin/env node
require('colors');
const apiHost = 'https://api.bigcommerce.com';
const dotStencilFilePath = './.stencil';
const options = { dotStencilFilePath };
const pkg = require('../package.json');
const Program = require('commander');
const stencilPush = require('../lib/stencil-push');
const versionCheck = require('../lib/version-check');
Program
.version(pkg.version)
.option('--host [hostname]', 'specify the api host', apiHost)
.option('-f, --file [filename]', 'specify the filename of the bundle to upload')
.option('-a, --activate [variationname]', 'specify the variation of the theme to activate')
.parse(process.argv);
if (!versionCheck()) {
return;
}
stencilPush(Object.assign({}, options, {
apiHost: Program.host || apiHost,
bundleZipPath: Program.file,
activate: Program.activate,
}), (err, result) => {
if (err) {
console.log('not ok'.red + ` -- ${err}`);
console.log('Please try again. If this error persists, please visit https://github.com/bigcommerce/stencil-cli/issues and submit an issue.');
} else {
console.log('ok'.green + ` -- ${result}`);
}
});
Inside /lib/stencil-push.js:
'use strict';
const _ = require('lodash');
const async = require('async');
const Bundle = require('./stencil-bundle');
const fs = require('fs');
const Inquirer = require('inquirer');
const os = require('os');
const ProgressBar = require('progress');
const themeApiClient = require('./theme-api-client');
const themePath = process.cwd();
const themeConfig = require('./theme-config').getInstance(themePath);
const uuid = require('uuid4');
const utils = {};
const Wreck = require('wreck');
const bar = new ProgressBar('Processing [:bar] :percent; ETA: :etas', {
complete: '=',
incomplete: ' ',
total: 100,
});
module.exports = utils;
function validateOptions(options, fields) {
options = options || {};
fields = fields || [];
fields.forEach(field => {
if (!_.has(options, field)) {
throw new Error(`${field} is required!`);
}
});
return options;
}
utils.readStencilConfigFile = (options, callback) => {
options = validateOptions(options, ['dotStencilFilePath']);
fs.readFile(options.dotStencilFilePath, { encoding: 'utf8' }, (err, data) => {
if (err) {
err.name = 'StencilConfigReadError';
return callback(err);
}
callback(null, Object.assign({}, options, {
config: JSON.parse(data),
}));
});
};
utils.getStoreHash = (options, callback) => {
options = validateOptions(options, ['config.normalStoreUrl']);
Wreck.get(`${options.config.normalStoreUrl}/admin/oauth/info`, { json: true, rejectUnauthorized: false }, (error, response, payload) => {
if (error) {
error.name = 'StoreHashReadError';
return callback(error);
}
if (response.statusCode !== 200 || !payload.store_hash) {
const err = new Error('Failed to retrieve store hash');
err.name = 'StoreHashReadError';
return callback(err);
}
callback(null, Object.assign({}, options, { storeHash: payload.store_hash }));
});
};
utils.getThemes = (options, callback) => {
const config = options.config;
themeApiClient.getThemes({
accessToken: config.accessToken,
apiHost: options.apiHost,
clientId: config.clientId,
storeHash: options.storeHash,
}, (error, result) => {
if (error) {
return callback(error);
}
callback(null, Object.assign({}, options, {
themes: result.themes,
}));
});
};
utils.generateBundle = (options, callback) => {
let bundle;
if (options.bundleZipPath) {
return async.nextTick(callback.bind(null, null, options));
}
bundle = new Bundle(themePath, themeConfig, themeConfig.getRawConfig(), {
dest: os.tmpdir(),
name: uuid(),
});
bundle.initBundle((err, bundleZipPath) => {
if (err) {
err.name = 'BundleInitError';
return callback(err);
}
callback(null, Object.assign(options, { bundleZipPath: options.bundleZipPath || bundleZipPath }));
});
};
utils.uploadBundle = (options, callback) => {
const config = options.config;
themeApiClient.postTheme({
accessToken: config.accessToken,
apiHost: options.apiHost,
bundleZipPath: options.bundleZipPath,
clientId: config.clientId,
storeHash: options.storeHash,
}, (error, result) => {
if (error) {
error.name = 'ThemeUploadError';
return callback(error);
}
callback(null, Object.assign({}, options, {
jobId: result.jobId,
themeLimitReached: !!result.themeLimitReached,
}));
});
};
utils.notifyUserOfThemeLimitReachedIfNecessary = (options, callback) => {
if (options.themeLimitReached) {
console.log('warning'.yellow + ` -- You have reached your upload limit. In order to proceed, you'll need to delete at least one theme.`);
}
return async.nextTick(callback.bind(null, null, options));
};
utils.promptUserToDeleteThemesIfNecessary = (options, callback) => {
if (!options.themeLimitReached) {
return async.nextTick(callback.bind(null, null, options));
}
const questions = [{
choices: options.themes.map(theme => ({
disabled: theme.is_active || !theme.is_private,
name: theme.name,
value: theme.uuid,
})),
message: 'Which theme(s) would you like to delete?',
name: 'themeIdsToDelete',
type: 'checkbox',
validate: (val) => {
if (val.length > 0) {
return true;
} else {
return 'You must delete at least one theme';
}
},
}];
Inquirer.prompt(questions, (answers) => {
callback(null, Object.assign({}, options, answers));
});
};
utils.deleteThemesIfNecessary = (options, callback) => {
const config = options.config;
if (!options.themeLimitReached) {
return async.nextTick(callback.bind(null, null, options));
}
async.parallel(options.themeIdsToDelete.map(themeId => {
return cb => {
themeApiClient.deleteThemeById(Object.assign({
accessToken: config.accessToken,
apiHost: options.apiHost,
clientId: config.clientId,
storeHash: options.storeHash,
themeId,
}, options), cb);
}
}), err => {
if (err) {
err.name = 'ThemeDeletionError';
return callback(err);
}
callback(null, options);
})
};
utils.uploadBundleAgainIfNecessary = (options, callback) => {
if (!options.themeLimitReached) {
return async.nextTick(callback.bind(null, null, options));
}
utils.uploadBundle(options, callback);
};
utils.notifyUserOfThemeUploadCompletion = (options, callback) => {
console.log('ok'.green + ' -- Theme Upload Finished');
return async.nextTick(callback.bind(null, null, options));
};
utils.markJobProgressPercentage = percentComplete => {
bar.update(percentComplete / 100);
};
utils.markJobComplete = () => {
utils.markJobProgressPercentage(100);
console.log('ok'.green + ' -- Theme Processing Finished');
};
utils.pollForJobCompletion = () => {
return async.retryable({
interval: 1000,
errorFilter: err => {
if (err.name === "JobCompletionStatusCheckPendingError") {
utils.markJobProgressPercentage(err.message);
return true;
}
return false;
},
times: Number.POSITIVE_INFINITY,
}, utils.checkIfJobIsComplete);
};
utils.checkIfJobIsComplete = (options, callback) => {
const config = options.config;
themeApiClient.getJob({
accessToken: config.accessToken,
apiHost: options.apiHost,
clientId: config.clientId,
storeHash: options.storeHash,
bundleZipPath: options.bundleZipPath,
jobId: options.jobId,
}, (error, result) => {
if (error) {
return callback(error);
}
utils.markJobComplete();
callback(null, Object.assign({}, options, result));
});
};
utils.promptUserWhetherToApplyTheme = (options, callback) => {
if (options.activate) {
callback(null, Object.assign({}, options, { applyTheme: true }));
} else {
const questions = [{
type: 'confirm',
name: 'applyTheme',
message: `Would you like to apply your theme to your store at ${options.config.normalStoreUrl}?`,
default: false,
}];
Inquirer.prompt(questions, answers => {
callback(null, Object.assign({}, options, { applyTheme: answers.applyTheme }));
});
};
};
utils.getVariations = (options, callback) => {
if (!options.applyTheme) {
return async.nextTick(callback.bind(null, null, options));
}
themeApiClient.getVariationsByThemeId({
accessToken: options.accessToken,
apiHost: options.apiHost,
clientId: options.clientId,
themeId: options.themeId,
storeHash: options.storeHash,
}, (error, result) => {
if (error) {
return callback(error);
};
if (options.activate !== true && options.activate !== undefined) {
const findVariation = result.variations.find(item => item.name === options.activate);
callback(null, Object.assign({}, options, { variationId: findVariation.uuid }));
} else if (options.activate === true) {
callback(null, Object.assign({}, options, { variationId: result.variations[0].uuid }));
} else {
callback(null, Object.assign({}, options, result));
};
});
};
utils.promptUserForVariation = (options, callback) => {
if (!options.applyTheme) {
return async.nextTick(callback.bind(null, null, options))
}
if (options.variationId) {
callback(null, options);
} else {
const questions = [{
type: 'list',
name: 'variationId',
message: 'Which variation would you like to apply?',
choices: options.variations.map(variation => ({ name: variation.name, value: variation.uuid })),
}];
Inquirer.prompt(questions, answers => {
console.log(answers);
callback(null, Object.assign({}, options, answers));
});
};
};
utils.requestToApplyVariationWithRetrys = () => {
return async.retryable({
interval: 1000,
errorFilter: err => {
if (err.name === "VariationActivationTimeoutError") {
console.log('warning'.yellow + ` -- Theme Activation Timed Out; Retrying...`);
return true;
}
return false;
},
times: 3,
}, utils.requestToApplyVariation);
};
utils.requestToApplyVariation = (options, callback) => {
if (!options.applyTheme) {
return async.nextTick(callback.bind(null, null, options));
}
themeApiClient.activateThemeByVariationId({
accessToken: options.accessToken,
apiHost: options.apiHost,
clientId: options.clientId,
storeHash: options.storeHash,
variationId: options.variationId,
}, (error, result) => {
if (error) {
return callback(error);
}
callback(null, Object.assign({}, options, result));
});
};
utils.notifyUserOfCompletion = (options, callback) => {
callback(null, 'Stencil Push Finished');
};
This allowed me to use something like stencil push --activate bold to specify a variation and skip all of the prompts.
As of version 1.15.1 this seems to be available with the -a, --activate [variationname] switch for stencil push
> stencil push -a "My Variant" worked for me
Thanks Nikita Puza!
It works like a charm. I applied the changes on stencil 1.14.1 version and the source code looks exactly the same.
The only difference is the second file is called stencil-push.utils.js instead of stencil-push.js
Related
How to pass geolocation permission to react-native-webview?
When using the geolcoation api in a react-native-webview, I am asked twice whether the app is allowed to use my current location. How is it possible to forward the already (not) granted permission to the webview? I am currently using react-native 0.68 and react-native-webview 11.22. First prompt: Second prompt: I should only be asked once for permission to use my current geolocation.
In case somebody faces the same problem, I solved this issue with the following workaround. I injected a custome javacscript into the webview to override the used geolocation api in the webview. My custome script does all the communication with the app. The app returns the geolocation and so the webview doesn't need to aks for permission. Custome Script export const getGeoLocationJS = () => { const getCurrentPosition = ` navigator.geolocation.getCurrentPosition = (success, error, options) => { window.ReactNativeWebView.postMessage(JSON.stringify({ event: 'getCurrentPosition', options: options })); window.addEventListener('message', (e) => { let eventData = {} try { eventData = JSON.parse(e.data); } catch (e) {} if (eventData.event === 'currentPosition') { success(eventData.data); } else if (eventData.event === 'currentPositionError') { error(eventData.data); } }); }; true; `; const watchPosition = ` navigator.geolocation.watchPosition = (success, error, options) => { window.ReactNativeWebView.postMessage(JSON.stringify({ event: 'watchPosition', options: options })); window.addEventListener('message', (e) => { let eventData = {} try { eventData = JSON.parse(e.data); } catch (e) {} if (eventData.event === 'watchPosition') { success(eventData.data); } else if (eventData.event === 'watchPositionError') { error(eventData.data); } }); }; true; `; const clearWatch = ` navigator.geolocation.clearWatch = (watchID) => { window.ReactNativeWebView.postMessage(JSON.stringify({ event: 'clearWatch', watchID: watchID })); }; true; `; return ` (function() { ${getCurrentPosition} ${watchPosition} ${clearWatch} })(); `; }; Webview import Geolocation from '#react-native-community/geolocation'; let webview = null; <WebView geolocationEnabled={ true } injectedJavaScript={ getGeoLocationJS() } javaScriptEnabled={ true } onMessage={ event => { let data = {} try { data = JSON.parse(event.nativeEvent.data); } catch (e) { console.log(e); } if (data?.event && data.event === 'getCurrentPosition') { Geolocation.getCurrentPosition((position) => { webview.postMessage(JSON.stringify({ event: 'currentPosition', data: position })); }, (error) => { webview.postMessage(JSON.stringify({ event: 'currentPositionError', data: error })); }, data.options); } else if (data?.event && data.event === 'watchPosition') { Geolocation.watchPosition((position) => { webview.postMessage(JSON.stringify({ event: 'watchPosition', data: position })); }, (error) => { webview.postMessage(JSON.stringify({ event: 'watchPositionError', data: error })); }, data.options); } else if (data?.event && data.event === 'clearWatch') { Geolocation.clearWatch(data.watchID); } }} ref={ ref => { webview = ref; if (onRef) { onRef(webview) } }} source={ url } startInLoadingState={ true } />
'Location enable permission' alert disappear after few second(3 to 4 second) in react-native
I am using 'react-native-geolocation-service' library for enabling the location for the app, if location is disabled. So if location is disabled then permission alert is working fine in android but In IOS it is appear for few second like 2 or 3 second, after that it will close. Below is the sample of method. static hasLocationPermissionIOS = async () => { const status = await Geolocation.requestAuthorization('always'); if (status === 'granted') { return 'GRANTED'; } if (status === 'denied') { return 'DENIED'; } if (status === 'disabled') { return 'DISABLED'; } }; static hasLocationPermission = async () => { if (Platform.OS === 'ios') { Geolocation.requestAuthorization('whenInUse'); const hasPermission = await this.hasLocationPermissionIOS(); return hasPermission; } if (Platform.OS === 'android') { const hasPermission = await this.hasLocationPermissionAndroid(); return hasPermission; } return false; }; static hasLocationPermission = async () => { if (Platform.OS === 'ios') { Geolocation.requestAuthorization('whenInUse'); const hasPermission = await this.hasLocationPermissionIOS(); return hasPermission; } if (Platform.OS === 'android') { const hasPermission = await this.hasLocationPermissionAndroid(); return hasPermission; } return false; }; static getLocation = async () => { const hasLocationPermission = await this.hasLocationPermission(); if (!hasLocationPermission) { return; } return new Promise((resolve, reject = (error) => {}) => { Geolocation.getCurrentPosition((position)=> { resolve(position); }, (error)=>{ resolve(error); }, { accuracy: { android: 'high', ios: 'best', }, enableHighAccuracy: true, timeout: 15000, maximumAge: 10000, distanceFilter: 0, forceRequestLocation: true, showLocationDialog: true, }); }); }; I referred the link but not able to find solution, https://github.com/douglasjunior/react-native-get-location/issues/18 Thanks in advance!!!
Voice Javascript SDK: using device.updateOptions() to change incoming sound to emulate a call waiting notification
Trying to implement simple call waiting with a different incoming sound for any calls that come in while the device is already on an active call. Upon the answer event we are using device.updateOptions() to update the sound, and the same to revert it back on the disconnect event. Out of the box we would get the standard incoming sound for that second call. But using the below we do not get any sound on the 2nd incoming call while there is an active call. I'm not sure what we are missing or if there is a better approach. const appReducer = (state, action) => { switch (action.type) { case SET_DEVICE: return { ...state, device: action.payload, }; case SET_ADMIN_DETAIL: return { ...state, adminDetail: action.payload, }; case SET_INCOMING_CALL: { // when incoming calls set to null (or after calls end) if (!action.payload.call) { /** if two calls in progress we need to check which one we have set to null so compare the outboundConnectionId ids with saved incoming call **/ if (state.incomingCall && state.incomingCall.outboundConnectionId === action.payload.prevStatusCall.outboundConnectionId) { return { ...state, incomingCall: null, }; } else if (state.secondIncomingCall && state.secondIncomingCall.outboundConnectionId === action.payload.prevStatusCall.outboundConnectionId) { /** if two calls in progress we need to check which one we have set to null so compare the outboundConnectionId ids with saved second incoming call **/ return { ...state, secondIncomingCall: null, }; } } // when incoming calls intialized or accept else { /** if there is not any call (incomingCall, enqueuedCall, outgoingCall, secondIncomingCall) saved in our state then treated as first call or update if first call outboundConnectionId is equal to call passed in params **/ if ( (!state.incomingCall && !state.enqueuedCall && !state.outgoingCall && !state.secondIncomingCall) || (state.incomingCall && action.payload.call.outboundConnectionId === state.incomingCall.outboundConnectionId) ) { if (action.payload.event === "accept") { console.log("First Call Accepted>>>>>>>>>>>>>>>>>>>>>"); state.device.updateOptions({ sounds: { incoming: "https://method-platform.s3.us-east-1.amazonaws.com/phone-configuration/audios/Short%20Marimba%20Notification%20Ding.mp3", disconnect: "https://method-platform.s3.us-east-1.amazonaws.com/phone-configuration/audios/call%20connect.mp3", outgoing: "https://method-platform.s3.us-east-1.amazonaws.com/phone-configuration/audios/call%20disconnect.mp3", }, }); } return { ...state, incomingCall: action.payload.call, }; } else { /** if we cancel/end the first call then we treate the second call as first **/ if (!state.incomingCall) { return { ...state, incomingCall: action.payload.call, secondIncomingCall: null, }; } else { /** else save the call obj in second call and use for the UI **/ return { ...state, secondIncomingCall: action.payload.call, }; } } } // return { // ...state, // incomingCall: action.payload.call, // }; } case SET_MISSED_CALL_FLAG: return { ...state, missedCallFlag: action.payload, }; case SET_OUT_GOING_CALL: { if (action.payload.event === "accept") { console.log("First Call Accepted>>>>>>>>>>>>>>>>>>>>>"); state.device.updateOptions({ sounds: { incoming: "https://method-platform.s3.us-east-1.amazonaws.com/phone-configuration/audios/Short%20Marimba%20Notification%20Ding.mp3", disconnect: "https://method-platform.s3.us-east-1.amazonaws.com/phone-configuration/audios/call%20connect.mp3", outgoing: "https://method-platform.s3.us-east-1.amazonaws.com/phone-configuration/audios/call%20disconnect.mp3", }, }); } return { ...state, outgoingCall: action.payload.call, }; } case SET_CALL_TIME: return { ...state, callTime: action.payload === 0 ? 0 : state.callTime + action.payload, }; case SET_ACTIVE: return { ...state, isActive: action.payload, }; case SET_DISPLAY_LIVE_CHAT_PANEL: return { ...state, displayLiveChatPanel: action.payload, }; case SET_DISPLAY_ON_CALL_PANEL: return { ...state, displayOnCallPanel: action.payload, }; case SET_MUTE: return { ...state, mute: action.payload, }; case SET_ENQUEUED: return { ...state, enqueued: action.payload, }; case SET_CALL_ENQUEUED: return { ...state, enqueuedCall: action.payload, enqueued: action.payload ? true : false, }; case SET_OUT_GOING_USER: return { ...state, selectedOutgoingUser: action.payload, }; default: return state; } }; export const AppContext = createContext(initialState); export const AppContextProvider = ({ children }) => { const [state, dispatch] = useReducer(appReducer, initialState); const location = useLocation(); const dispatchFn = useDispatch(); function setAdminDetail(payload) { dispatch({ type: SET_ADMIN_DETAIL, payload: payload }); } function setDevice(payload) { dispatch({ type: SET_DEVICE, payload: payload }); } function setIncomingCall(payload) { dispatch({ type: SET_INCOMING_CALL, payload: payload }); } function setMissedCallFlag(payload) { dispatch({ type: SET_MISSED_CALL_FLAG, payload: payload }); } function setOutgoingCall(payload) { dispatch({ type: SET_OUT_GOING_CALL, payload: payload }); } function setCallTime(payload) { dispatch({ type: SET_CALL_TIME, payload: payload }); } function setIsActive(payload) { dispatch({ type: SET_ACTIVE, payload: payload }); } function setDisplayOnCallPanel(payload) { dispatch({ type: SET_DISPLAY_ON_CALL_PANEL, payload: payload }); } function setDisplayLiveChatPanel(payload) { dispatch({ type: SET_DISPLAY_LIVE_CHAT_PANEL, payload: payload }); } function setMute(payload) { dispatch({ type: SET_MUTE, payload: payload }); } function setCallEnqueue(payload) { dispatch({ type: SET_CALL_ENQUEUED, payload: payload }); } function setEnqueue(payload) { dispatch({ type: SET_ENQUEUED, payload: payload }); } function setSelectedOutgoingUser(payload) { dispatch({ type: SET_OUT_GOING_USER, payload: payload }); } const setUpDevice = async (adminDetail) => { const result = await getData( `${process.env.REACT_APP_TWILIO_SERVER}token?extension_name=${adminDetail?.twilio_call_extension?.name}&extension_id=${adminDetail?.twilio_call_extension?.id}` ); const newDevice = new Device(result.token, { sounds: { disconnect: "https://method-platform.s3.us-east-1.amazonaws.com/phone-configuration/audios/call%20connect.mp3", outgoing: "https://method-platform.s3.us-east-1.amazonaws.com/phone-configuration/audios/call%20disconnect.mp3", }, logLevel: 1, allowIncomingWhileBusy: true, }); newDevice.register(); newDevice.on("registered", (twilioError, call) => { console.log("registered: ", twilioError); }); newDevice.on("error", (twilioError, call) => { console.log("An error has occurred: ", twilioError); }); newDevice.audio.on("", (twilioError, call) => { console.log("An error has occurred: ", twilioError, call); }); newDevice.on("incoming", (call) => { setMissedCallFlag(true); setDisplayLiveChatPanel(false); call.on("accept", (call) => { setIncomingCall({ call, event: "accept" }); setIsActive(true); }); call.on("cancel", () => { setDisplayOnCallPanel(false); setIncomingCall({ call: null, prevStatusCall: call }); setIsActive(false); setCallTime(0); dispatch( getRecentCalls({ id: adminDetail.twilio_call_extension.id, offset: 0, }) ); }); call.on("error", (error) => { console.log("An error has occurred: ", error); }); call.on("disconnect", (call) => { setDisplayOnCallPanel(false); setIncomingCall({ call: null, prevStatusCall: call, event: "disconnect" }); setIsActive(false); setCallTime(0); dispatch( getRecentCalls({ id: adminDetail.twilio_call_extension.id, offset: 0, }) ); }); call.on("reject", () => { setDisplayOnCallPanel(false); console.log("The call was rejected."); setIncomingCall({ call: null, prevStatusCall: call }); setIsActive(false); setCallTime(0); dispatch( getRecentCalls({ id: adminDetail.twilio_call_extension.id, offset: 0, }) ); }); call.on("reconnected", () => { console.log("The call has regained connectivity."); }); setIncomingCall({ call }); }); newDevice.on("tokenWillExpire", async () => { console.log("acascas"); let updatedToken = await getData( `${process.env.REACT_APP_TWILIO_SERVER}token?extension_name=${adminDetail?.twilio_call_extension?.name}&extension_id=${adminDetail?.twilio_call_extension?.id}` ); newDevice.updateToken(updatedToken.token); setDevice(newDevice); }); setDevice(newDevice); }; const setCallOnMute = async () => { let { incomingCall, outgoingCall, mute } = state; let muted = !mute; if (incomingCall) { incomingCall.mute(muted); setMute(muted); } else if (outgoingCall) { outgoingCall.mute(muted); setMute(muted); } }; const enqueueCall = async (call_id) => { const result = await getData(`${process.env.REACT_APP_TWILIO_SERVER}enqueue-call/${call_id}`); if (result.status === 200) { console.log(result); setCallEnqueue(result.data); setEnqueue(true); return result.data; } else { return null; } }; const dialNumber = async (number, id, name) => { let { adminDetail, device } = state; let From = adminDetail?.twilio_call_extension?.direct_number; let params = { To: number, From: From, outboundcall: "true", extension_id: adminDetail?.twilio_call_extension?.id, createdBy: adminDetail?.id, }; if (id) { params.userId = id; } if (name) { params.tempName = name; } const call = await device.connect({ params: params, }); setOutGoingCallState(call); setDisplayLiveChatPanel(false); }; const setOutGoingCallState = (call) => { setOutgoingCall({ call: null }); setIsActive(true); setMute(false); setOutgoingCall({ call: call }); call.on("disconnect", (nest_call) => { setDisplayOnCallPanel(false); console.log("disconnect"); setOutgoingCall({ call: null }); setIsActive(false); setMute(false); setSelectedOutgoingUser(null); setCallTime(0); dispatch( getRecentCalls({ id: state.adminDetail.twilio_call_extension.id, offset: 0, }) ); }); call.on("accept", (nest_call) => { setOutgoingCall({ call: nest_call, event: "accept" }); }); call.on("cancel", (nest_call) => { setDisplayOnCallPanel(false); setOutgoingCall({ call: null }); setIsActive(false); setMute(false); setSelectedOutgoingUser(null); setCallTime(0); }); }; useEffect(() => { let interval = null; if (state.isActive) { interval = setInterval(() => { setCallTime(1000); }, 1000); } else { clearInterval(interval); } return () => { clearInterval(interval); }; }, [state.isActive]); useEffect(() => { let interval = null; if (state.isActive) { interval = setInterval(() => { setCallTime(1000); }, 1000); } else { clearInterval(interval); } return () => { clearInterval(interval); }; }, [state.isActive]); // reset optons when there is no active call useEffect(() => { if (state.device && !state.incomingCall && !state.secondIncomingCall && !state.enqueuedCall && !state.outgoingCall) { console.log("Both Calls Ends >>>>>>>>>>>>>>>>>>>>>"); state.device.updateOptions({ sounds: { disconnect: "https://method-platform.s3.us-east-1.amazonaws.com/phone-configuration/audios/call%20connect.mp3", outgoing: "https://method-platform.s3.us-east-1.amazonaws.com/phone-configuration/audios/call%20disconnect.mp3", }, }); } },
Loopback 4 implementing Microsoft Graph API
I am currently building a microservice that is responsible to communicate with Microsoft Graph, I have already made one with Loopback 3 and this was not a problem. Except now, I am trying to do the same thing but with Loopback 4, but since the language changes from JavaScript to TypeScript I don't know if it's still possible to achieve this. This was the code I used for Loopback 3 in my root server file: 'use strict'; const express = require('express'); const erouter = require('express').Router(); var session = require('express-session'); var passport = require('passport'); var OIDCStrategy = require('passport-azure-ad').OIDCStrategy; const request = require('request'); var querystring = require('querystring'); const graph = require('./graph.service'); const getBookings = require('./getBookings.service'); const cors = require('cors'); var compression = require('compression'); module.exports = function(server) { // Install a `/` route that returns server status var router = server.loopback.Router(); router.get('/', server.loopback.status()); // Configure simple-oauth2 const oauth2 = require('simple-oauth2').create({ client: { id: process.env.OAUTH_APP_ID, secret: process.env.OAUTH_APP_PASSWORD }, auth: { tokenHost: process.env.OAUTH_AUTHORITY, authorizePath: process.env.OAUTH_AUTHORIZE_ENDPOINT, tokenPath: process.env.OAUTH_TOKEN_ENDPOINT } }); passport.serializeUser(function(user, done) { var MSUser = server.models.MSUser; var id = user.profile.oid; MSUser.find({ where: { oid: id } }, function(err, msu) { if (err) return done(err, null); if (!msu) { MSUser.create(user); } else { done(null, id); } }); }); passport.deserializeUser(function(id, done) { var MSUser = server.models.MSUser; MSUser.findById(id, function(err, user) { if (err) return next(err); done(null, user); }); }); async function signInComplete(iss, sub, profile, accessToken, refreshToken, params, done) { if (!profile.oid) { return done(new Error("No OID found in user profile."), null); } try { const user = await graph.getUserDetails(accessToken); if (user) { profile['email'] = user.mail ? user.mail.toLowerCase() : user.userPrincipalName.toLowerCase(); } } catch (err) { done(err, null); } let oauthToken = oauth2.accessToken.create(params); var AuthUser = server.models.AuthUser; var user = {}; AuthUser.find({ where: { email: profile['email'] } }, function(err, au) { if (err) return done(err, null); if (au.length != 1) return done(new Error("User was not found with that email address."), null); user = au[0]; const dataMsAuth = querystring.stringify({ "created": new Date().toDateString(), "token_type": oauthToken.token.token_type, "expires_in": oauthToken.token.expires_in, "access_token": oauthToken.token.access_token, "scope": oauthToken.token.scope, "ext_expires_in": oauthToken.token.ext_expires_in, "refresh_token": oauthToken.token.refresh_token, "id_token": oauthToken.token.id_token, "expires_at": new Date(oauthToken.token.expires_at).toDateString() }); const postMSAuth = { url: process.env.API_URL + "api/Companies/" + user.companyId + "/msauth", method: 'POST', body: dataMsAuth, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } request(postMSAuth, function(err, resp, body) { if (err) return done(err, null); var MSUser = server.models.MSUser; var id = profile.oid; var msuser = { profile, oauthToken, oid: id, email: profile.email } MSUser.findById(id, function(err, msu) { if (err) return done(err, null); if (!msu) { MSUser.create(msuser); } }); return done(null, msuser); }); }); } passport.use(new OIDCStrategy({ identityMetadata: `${process.env.OAUTH_AUTHORITY}${process.env.OAUTH_ID_METADATA}`, clientID: process.env.OAUTH_APP_ID, responseType: 'code id_token', responseMode: 'form_post', redirectUrl: process.env.OAUTH_REDIRECT_URI, allowHttpForRedirectUrl: true, clientSecret: process.env.OAUTH_APP_PASSWORD, validateIssuer: false, passReqToCallback: false, scope: process.env.OAUTH_SCOPES.split(' ') }, signInComplete )); var app = express(); app.use(compression()); app.use(session({ secret: process.env.BOOKINGS_LOOPBACK_SECRET, resave: false, saveUninitialized: false, unset: 'destroy' })); app.use("/result", express.static('client')); app.use(passport.initialize()); app.use(passport.session()); app.use(cors({ origin: '*' })); erouter.get('/API/bookings/:companyid', getBookings()); erouter.get('/auth/signin', function(req, res, next) { passport.authenticate('azuread-openidconnect', { response: res, prompt: 'login', state: req.query.state, failureRedirect: process.env.WEBSITE_URL + 'settings?error=incorrect_request', successRedirect: process.env.WEBSITE_URL + 'settings?auth=success' })(req, res, next); } ); erouter.post('/auth/callback', function(req, res, next) { passport.authenticate('azuread-openidconnect', { response: res, failureRedirect: process.env.WEBSITE_URL + 'settings?error=permission_denied', successRedirect: process.env.WEBSITE_URL + 'settings?auth=success' })(req, res, next); } ); app.use(erouter); server.use(app); server.use(router); }; So my question is: "Is it possible to implement Microsoft Graph API in TypeScript using Loopback 4 or should I keep using Loopback 3 In JavaScript?" Thanks in advance, Billy Cottrell
IOS Notification Permission alert does not show
SDK Version: 39.0.0 Platforms(Android/iOS/web/all): All I am not getting accept or decline notifications permissions alert when loading my app in production. I have tried clearing certificates and keys and allowing expo to add everything from a clean slate, but still no luck. I am starting to think maybe it’s my code which is the reason why the alert doesn’t get fired. import Constants from "expo-constants"; import * as Notifications from "expo-notifications"; import { Permissions } from "expo-permissions"; import { Notifications as Notifications2 } from "expo"; Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, shouldPlaySound: true, shouldSetBadge: false }) }); export default class LoginScreen extends React.Component { state = { email: "", password: "", notification: {}, errorMessage: null }; async componentDidMount() { this.registerForPushNotificationsAsync(); //Notifications.addNotificationReceivedListener(this._handleNotification); Notifications2.addListener(data => { this.setState({ notification: data }); }); Notifications.addNotificationResponseReceivedListener( this._handleNotificationResponse ); } _handleNotification = notification => { this.setState({ notification: notification }); }; _handleNotificationResponse = response => { console.log(response); }; handleLogin = async () => { try { const { email, password } = this.state; const expoPushToken = await Notifications.getExpoPushTokenAsync(); console.log(expoPushToken); const userinfo = await firebase .auth() .signInWithEmailAndPassword(email, password); console.log("user ID ", userinfo.user.uid); await firebase .firestore() .collection("users") .doc(userinfo.user.uid.toString()) .update({ expo_token: expoPushToken["data"] }) .then(function() { console.log("token successfully updated!"); }) .catch(function(error) { // The document probably doesn't exist. console.error("Error updating document: ", error); }); } catch (error) { console.log("=======Error in login", error); this.setState({ errorMessage: error.message }); } }; registerForPushNotificationsAsync = async () => { if (Constants.isDevice) { const { status: existingStatus } = await Permissions.getAsync( Permissions.NOTIFICATIONS ); let finalStatus = existingStatus; if (existingStatus !== "granted") { const { status } = await Permissions.askAsync( Permissions.NOTIFICATIONS ); finalStatus = status; } if (finalStatus !== "granted") { alert("Failed to get push token for push notification!"); return; } const token = await Notifications.getExpoPushTokenAsync(); console.log(token); //this.setState({ expoPushToken: token }); } else { alert("Must use physical device for Push Notifications"); } if (Platform.OS === "android") { Notifications.setNotificationChannelAsync("default", { name: "default", importance: Notifications.AndroidImportance.MAX, vibrationPattern: [0, 250, 250, 250], lightColor: "#FF231F7C" }); } };
import { Permissions } from "expo-permissions"; should of been import * as Permissions from 'expo-permissions'; Sometimes we all make simple mistakes.