I'm building a flutter app and have encountered a weird problem that I haven't seen before. I'm using Firebase auth for phone verification through OTP.
However, I only receive OTPs on the debug mode for iOS and not the release mode. It works fine on Android in both cases. I'm not sure what I'm doing wrong as I have the updated GoogleService-info.plist in my iOS root. Do you guys have any ideas on how I can make it work?
This is my workflow when I verify the OTP:-
Future<void> verifyPhone(String phoneNumber, ) async {
await FirebaseAuth.instance.verifyPhoneNumber(
phoneNumber: phoneNumber,
timeout: const Duration(seconds: 60),
verificationCompleted: (PhoneAuthCredential credential) {
print('Auth completed');
},
verificationFailed: (FirebaseAuthException e) {
print(e);
print('Auth failed');
},
codeSent: (String verificationId, int? resendToken) {
print('OTP sent');
verId = verificationId;
Queries.instance.verId = verificationId;
print(verId + '........');
},
codeAutoRetrievalTimeout: (String verificationId) {
print('Timeout');
}
);
}
For some Firebase related functions to work in iOS, you will have to link your Firebase project with Apple Push Notification Service's APN key.
Things you can do,
Go to Apple Developer Console and create an APN key.
Export the key and add it to Firebase Console.
For reference,
https://www.kodeco.com/20201639-firebase-cloud-messaging-for-ios-push-notifications
Below-mentioned is my function for OTP verification:
verifyPhoneNumber() async {
await FirebaseAuth.instance.verifyPhoneNumber(
phoneNumber: widget.phoneNumberControllerValueText,
verificationCompleted: (PhoneAuthCredential credential) async {
UserCredential userCredential =
await FirebaseAuth.instance.signInWithCredential(credential);
if (userCredential.user != null) {
log('verifyPhoneNumber - User logged in.');
if (mounted) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) {
return const HomeScreen();
},
),
(Route<dynamic> route) => false,
);
}
}
},
verificationFailed: (FirebaseAuthException e) {
log('e.toString: ${e.message.toString()}');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
e.message.toString(),
),
),
);
if (e.code == 'invalid-phone-number') {
log('The provided phone number is not valid.');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'The provided phone number is not valid.',
),
),
);
}
},
codeSent: (String verificationId, int? resendToken) async {
tempVerificationId = verificationId;
},
codeAutoRetrievalTimeout: (String verificationId) {
tempVerificationId = verificationId;
},
timeout: const Duration(seconds: 60),
);
}
You can cross-verify with your function to check the difference, or else if you want to full source code, it is available at:
https://github.com/dharambudh1/firebase-otp-integration-demo/
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
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.
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
I have been following this guide to add and retrieve items from firebase using React Native. If I set my rules on firebase to public, everything works but if I set it to the following, I get a permission denied error.
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
I have added all of my config data properly in my ios.js. Is there some basic step that I am missing here?
index.ios.js:
// Initialize Firebase
const firebaseConfig = {
apiKey: 'myapikey',
authDomain: 'myauthdomain',
databaseURL: 'https://url.firebaseio.com',
projectId: 'myProjectId',
storageBucket: 'projectid.appspot.com',
messagingSenderId: 'myMessagingSenderId'
};
const firebaseApp = firebase.initializeApp(firebaseConfig);
class MyNewAppreactold extends Component {
constructor(props) {
super(props);
this.state = {
dataSource: new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2,
})
};
this.itemsRef = this.getRef().child('items');
}
getRef() {
return firebaseApp.database().ref();
}
listenForItems(itemsRef) {
itemsRef.on('value', (snap) => {
// get children as an array
var items = [];
snap.forEach((child) => {
items.push({
title: child.val().title,
_key: child.key
});
});
this.setState({
dataSource: this.state.dataSource.cloneWithRows(items)
});
});
}
// componentWillMount() {
// firebase.initializeApp(firebaseConfig);
// }
componentDidMount() {
this.listenForItems(this.itemsRef);
}
render() {
return (
<View style={styles.container}>
<StatusBar title="Grocery List" />
<ListView
dataSource={this.state.dataSource}
renderRow={this._renderItem.bind(this)}
enableEmptySections={true}
style={styles.listview}/>
<ActionButton onPress={this._addItem.bind(this)} title="Add" />
</View>
)
}
_addItem() {
AlertIOS.prompt(
'Add New Item',
null,
[
{text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel'},
{
text: 'Add',
onPress: (text) => {
this.itemsRef.push({ title: text })
}
},
],
'plain-text'
);
}
_renderItem(item) {
const onPress = () => {
AlertIOS.alert(
'Complete',
null,
[
{text: 'Complete', onPress: (text) => this.itemsRef.child(item._key).remove()},
{text: 'Cancel', onPress: (text) => console.log('Cancelled')}
]
);
};
return (
<ListItem item={item} onPress={onPress} />
);
}
}
AppRegistry.registerComponent('MyNewAppreactold', () => MyNewAppreactold);
The code you shared doesn't authenticate the user. Since your security rules require that the user is authenticated to be allowed access to the data, they reject the unauthenticated user of your app.
To solve this problem, you'll need to authenticate the user. The simplest way to do that is to sign the user in anonymously:
firebase.auth().signInAnonymously();
Then attach your database listeners after the user is authenticated:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
this.listenForItems(this.itemsRef);
} else {
// User is signed out.
// ...
}
// ...
});