nx-react-native-cli 2.6.2 → 2.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.cjs CHANGED
@@ -61,7 +61,7 @@ ${m.description}`:"";return`${[a,v,b].filter(Boolean).join(" ")}
61
61
  ${C}${T}${F}${N2.default.cursorHide}`});var b1=O(require("node:readline"),1),j=O(d1(),1),ml=O(m1(),1),E1=O(Ha(),1),F1=O(Et(),1),v1={set:(e,r="",t)=>{let n=e;r.split(".").forEach((i,u,o)=>{i==="__proto__"||i==="constructor"||(u===o.length-1?n[i]=t:i in n||(n[i]={}),n=n[i])})},get:(e,r="",t)=>{let n=u=>String.prototype.split.call(r,u).filter(Boolean).reduce((o,a)=>o!=null?o[a]:o,e),i=n(/[,[\]]+?/)||n(/[,.[\]]+?/);return i===void 0||i===e?t:i}};function Dl(e,r,t){if(r in e){let n=e[r];if(typeof n=="function")return(0,j.from)((0,ml.default)(n)(t).then(i=>Object.assign(e,{[r]:i})))}return(0,j.of)(e)}var hl=class extends Error{isTtyError=!0};function ES(e={}){e.skipTTYChecks=e.skipTTYChecks===void 0?!0:e.skipTTYChecks;let r=e.input||process.stdin;if(!e.skipTTYChecks&&!r.isTTY)throw new hl("Prompts can not be meaningfully rendered in non-TTY environments");let t=new E1.default;return t.pipe(e.output||process.stdout),{terminal:!0,...e,input:r,output:t}}function FS(e){return Object.values(e).every(r=>typeof r=="object"&&!Array.isArray(r)&&r!=null)}function _S(e){return e.prototype&&"run"in e.prototype&&typeof e.prototype.run=="function"}var Dn=class{prompts;answers={};process=j.EMPTY;onClose;opt;rl;constructor(r,t){this.opt=t,this.prompts=r}run(r,t){this.answers=typeof t=="object"?{...t}:{};let n;Array.isArray(r)?n=(0,j.from)(r):(0,j.isObservable)(r)?n=r:FS(r)?n=(0,j.from)(Object.entries(r).map(([u,o])=>Object.assign({},o,{name:u}))):n=(0,j.from)([r]),this.process=n.pipe((0,j.concatMap)(u=>this.processQuestion(u)));let i=(0,j.lastValueFrom)(this.process.pipe((0,j.reduce)((u,o)=>(v1.set(u,o.name,o.answer),u),this.answers))).then(()=>this.onCompletion(),u=>this.onError(u));return Object.assign(i,{ui:this})}onCompletion(){return this.close(),this.answers}onError(r){return this.close(),Promise.reject(r)}processQuestion(r){return r={...r},(0,j.defer)(()=>(0,j.of)(r).pipe((0,j.concatMap)(this.setDefaultType),(0,j.concatMap)(this.filterIfRunnable),(0,j.concatMap)(n=>Dl(n,"message",this.answers)),(0,j.concatMap)(n=>Dl(n,"default",this.answers)),(0,j.concatMap)(n=>Dl(n,"choices",this.answers)),(0,j.concatMap)(n=>("choices"in n&&(n.choices=n.choices.map(i=>typeof i=="string"?{name:i,value:i}:i)),(0,j.of)(n))),(0,j.concatMap)(n=>this.fetchAnswer(n))))}fetchAnswer(r){let t=this.prompts[r.type];if(t==null)throw new Error(`Prompt for type ${r.type} not found`);return _S(t)?(0,j.defer)(()=>{let n=b1.default.createInterface(ES(this.opt));n.resume();let i=()=>{n.removeListener("SIGINT",this.onForceClose),n.setPrompt(""),n.output.unmute(),n.output.write(F1.default.cursorShow),n.output.end(),n.close()};this.onClose=i,this.rl=n,process.on("exit",this.onForceClose),n.on("SIGINT",this.onForceClose);let u=new t(r,n,this.answers);return(0,j.from)(u.run().then(o=>(i(),this.onClose=void 0,this.rl=void 0,{name:r.name,answer:o})))}):(0,j.defer)(()=>(0,j.from)(t(r,this.opt).then(n=>({name:r.name,answer:n}))))}onForceClose=()=>{this.close(),process.kill(process.pid,"SIGINT"),console.log("")};close=()=>{process.removeListener("exit",this.onForceClose),typeof this.onClose=="function"&&this.onClose()};setDefaultType=r=>(this.prompts[r.type]||(r.type="input"),(0,j.defer)(()=>(0,j.of)(r)));filterIfRunnable=r=>{if(r.askAnswered!==!0&&v1.get(this.answers,r.name)!==void 0)return j.EMPTY;let{when:t}=r;return t===!1?j.EMPTY:typeof t!="function"?(0,j.of)(r):(0,j.defer)(()=>(0,j.from)((0,ml.default)(t)(this.answers).then(n=>{if(n)return r})).pipe((0,j.filter)(n=>n!=null)))}};var _1={input:zs,select:zi,list:zi,number:Ks,confirm:Gs,rawlist:Zs,expand:Xs,checkbox:ts,password:ec,editor:Hs};function y1(e){function r(t,n){let i=new Dn(r.prompts,e);try{return i.run(t,n)}catch(u){let o=Promise.reject(u);return Object.assign(o,{ui:i})}}return r.prompts={..._1},r.registerPrompt=function(t,n){return r.prompts[t]=n,this},r.restoreDefaultPrompts=function(){r.prompts={..._1}},r}var vl=y1();function yS(e,r){vl.registerPrompt(e,r)}function gS(){vl.restoreDefaultPrompts()}var CS={prompt:vl,ui:{Prompt:Dn},createPromptModule:y1,registerPrompt:yS,restoreDefaultPrompts:gS,Separator:ae},ri=CS;var ui=O(require("node:process"),1);var _l=O(require("node:process"),1);var k1=O(require("node:process"),1),T1=O(A1(),1),M1=O(P1(),1),AS=(0,T1.default)(()=>{(0,M1.default)(()=>{k1.default.stderr.write("\x1B[?25h")},{alwaysLast:!0})}),I1=AS;var la=!1,bn={};bn.show=(e=_l.default.stderr)=>{e.isTTY&&(la=!1,e.write("\x1B[?25h"))};bn.hide=(e=_l.default.stderr)=>{e.isTTY&&(I1(),la=!0,e.write("\x1B[?25l"))};bn.toggle=(e,r)=>{e!==void 0&&(la=e),la?bn.show(r):bn.hide(r)};var yl=bn;var oi=O(Ei(),1);var Se=O(require("node:process"),1);function gl(){return Se.default.platform!=="win32"?Se.default.env.TERM!=="linux":!!Se.default.env.CI||!!Se.default.env.WT_SESSION||!!Se.default.env.TERMINUS_SUBLIME||Se.default.env.ConEmuTask==="{cmd::Cmder}"||Se.default.env.TERM_PROGRAM==="Terminus-Sublime"||Se.default.env.TERM_PROGRAM==="vscode"||Se.default.env.TERM==="xterm-256color"||Se.default.env.TERM==="alacritty"||Se.default.env.TERMINAL_EMULATOR==="JetBrains-JediTerm"}var OS={info:z.blue("\u2139"),success:z.green("\u2714"),warning:z.yellow("\u26A0"),error:z.red("\u2716")},SS={info:z.blue("i"),success:z.green("\u221A"),warning:z.yellow("\u203C"),error:z.red("\xD7")},BS=gl()?OS:SS,ni=BS;function Cl({onlyFirst:e=!1}={}){let r=["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)","(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"].join("|");return new RegExp(r,e?void 0:"g")}var qS=Cl();function ii(e){if(typeof e!="string")throw new TypeError(`Expected a \`string\`, got \`${typeof e}\``);return e.replace(qS,"")}function R1(e){return e===161||e===164||e===167||e===168||e===170||e===173||e===174||e>=176&&e<=180||e>=182&&e<=186||e>=188&&e<=191||e===198||e===208||e===215||e===216||e>=222&&e<=225||e===230||e>=232&&e<=234||e===236||e===237||e===240||e===242||e===243||e>=247&&e<=250||e===252||e===254||e===257||e===273||e===275||e===283||e===294||e===295||e===299||e>=305&&e<=307||e===312||e>=319&&e<=322||e===324||e>=328&&e<=331||e===333||e===338||e===339||e===358||e===359||e===363||e===462||e===464||e===466||e===468||e===470||e===472||e===474||e===476||e===593||e===609||e===708||e===711||e>=713&&e<=715||e===717||e===720||e>=728&&e<=731||e===733||e===735||e>=768&&e<=879||e>=913&&e<=929||e>=931&&e<=937||e>=945&&e<=961||e>=963&&e<=969||e===1025||e>=1040&&e<=1103||e===1105||e===8208||e>=8211&&e<=8214||e===8216||e===8217||e===8220||e===8221||e>=8224&&e<=8226||e>=8228&&e<=8231||e===8240||e===8242||e===8243||e===8245||e===8251||e===8254||e===8308||e===8319||e>=8321&&e<=8324||e===8364||e===8451||e===8453||e===8457||e===8467||e===8470||e===8481||e===8482||e===8486||e===8491||e===8531||e===8532||e>=8539&&e<=8542||e>=8544&&e<=8555||e>=8560&&e<=8569||e===8585||e>=8592&&e<=8601||e===8632||e===8633||e===8658||e===8660||e===8679||e===8704||e===8706||e===8707||e===8711||e===8712||e===8715||e===8719||e===8721||e===8725||e===8730||e>=8733&&e<=8736||e===8739||e===8741||e>=8743&&e<=8748||e===8750||e>=8756&&e<=8759||e===8764||e===8765||e===8776||e===8780||e===8786||e===8800||e===8801||e>=8804&&e<=8807||e===8810||e===8811||e===8814||e===8815||e===8834||e===8835||e===8838||e===8839||e===8853||e===8857||e===8869||e===8895||e===8978||e>=9312&&e<=9449||e>=9451&&e<=9547||e>=9552&&e<=9587||e>=9600&&e<=9615||e>=9618&&e<=9621||e===9632||e===9633||e>=9635&&e<=9641||e===9650||e===9651||e===9654||e===9655||e===9660||e===9661||e===9664||e===9665||e>=9670&&e<=9672||e===9675||e>=9678&&e<=9681||e>=9698&&e<=9701||e===9711||e===9733||e===9734||e===9737||e===9742||e===9743||e===9756||e===9758||e===9792||e===9794||e===9824||e===9825||e>=9827&&e<=9829||e>=9831&&e<=9834||e===9836||e===9837||e===9839||e===9886||e===9887||e===9919||e>=9926&&e<=9933||e>=9935&&e<=9939||e>=9941&&e<=9953||e===9955||e===9960||e===9961||e>=9963&&e<=9969||e===9972||e>=9974&&e<=9977||e===9979||e===9980||e===9982||e===9983||e===10045||e>=10102&&e<=10111||e>=11094&&e<=11097||e>=12872&&e<=12879||e>=57344&&e<=63743||e>=65024&&e<=65039||e===65533||e>=127232&&e<=127242||e>=127248&&e<=127277||e>=127280&&e<=127337||e>=127344&&e<=127373||e===127375||e===127376||e>=127387&&e<=127404||e>=917760&&e<=917999||e>=983040&&e<=1048573||e>=1048576&&e<=1114109}function L1(e){return e===12288||e>=65281&&e<=65376||e>=65504&&e<=65510}function N1(e){return e>=4352&&e<=4447||e===8986||e===8987||e===9001||e===9002||e>=9193&&e<=9196||e===9200||e===9203||e===9725||e===9726||e===9748||e===9749||e>=9800&&e<=9811||e===9855||e===9875||e===9889||e===9898||e===9899||e===9917||e===9918||e===9924||e===9925||e===9934||e===9940||e===9962||e===9970||e===9971||e===9973||e===9978||e===9981||e===9989||e===9994||e===9995||e===10024||e===10060||e===10062||e>=10067&&e<=10069||e===10071||e>=10133&&e<=10135||e===10160||e===10175||e===11035||e===11036||e===11088||e===11093||e>=11904&&e<=11929||e>=11931&&e<=12019||e>=12032&&e<=12245||e>=12272&&e<=12287||e>=12289&&e<=12350||e>=12353&&e<=12438||e>=12441&&e<=12543||e>=12549&&e<=12591||e>=12593&&e<=12686||e>=12688&&e<=12771||e>=12783&&e<=12830||e>=12832&&e<=12871||e>=12880&&e<=19903||e>=19968&&e<=42124||e>=42128&&e<=42182||e>=43360&&e<=43388||e>=44032&&e<=55203||e>=63744&&e<=64255||e>=65040&&e<=65049||e>=65072&&e<=65106||e>=65108&&e<=65126||e>=65128&&e<=65131||e>=94176&&e<=94180||e===94192||e===94193||e>=94208&&e<=100343||e>=100352&&e<=101589||e>=101632&&e<=101640||e>=110576&&e<=110579||e>=110581&&e<=110587||e===110589||e===110590||e>=110592&&e<=110882||e===110898||e>=110928&&e<=110930||e===110933||e>=110948&&e<=110951||e>=110960&&e<=111355||e===126980||e===127183||e===127374||e>=127377&&e<=127386||e>=127488&&e<=127490||e>=127504&&e<=127547||e>=127552&&e<=127560||e===127568||e===127569||e>=127584&&e<=127589||e>=127744&&e<=127776||e>=127789&&e<=127797||e>=127799&&e<=127868||e>=127870&&e<=127891||e>=127904&&e<=127946||e>=127951&&e<=127955||e>=127968&&e<=127984||e===127988||e>=127992&&e<=128062||e===128064||e>=128066&&e<=128252||e>=128255&&e<=128317||e>=128331&&e<=128334||e>=128336&&e<=128359||e===128378||e===128405||e===128406||e===128420||e>=128507&&e<=128591||e>=128640&&e<=128709||e===128716||e>=128720&&e<=128722||e>=128725&&e<=128727||e>=128732&&e<=128735||e===128747||e===128748||e>=128756&&e<=128764||e>=128992&&e<=129003||e===129008||e>=129292&&e<=129338||e>=129340&&e<=129349||e>=129351&&e<=129535||e>=129648&&e<=129660||e>=129664&&e<=129672||e>=129680&&e<=129725||e>=129727&&e<=129733||e>=129742&&e<=129755||e>=129760&&e<=129768||e>=129776&&e<=129784||e>=131072&&e<=196605||e>=196608&&e<=262141}function jS(e){if(!Number.isSafeInteger(e))throw new TypeError(`Expected a code point, got \`${typeof e}\`.`)}function $1(e,{ambiguousAsWide:r=!1}={}){return jS(e),L1(e)||N1(e)||r&&R1(e)?2:1}var W1=O(V1(),1),PS=new Intl.Segmenter,kS=/^\p{Default_Ignorable_Code_Point}$/u;function wl(e,r={}){if(typeof e!="string"||e.length===0)return 0;let{ambiguousIsNarrow:t=!0,countAnsiEscapeCodes:n=!1}=r;if(n||(e=ii(e)),e.length===0)return 0;let i=0,u={ambiguousAsWide:!t};for(let{segment:o}of PS.segment(e)){let a=o.codePointAt(0);if(!(a<=31||a>=127&&a<=159)&&!(a>=8203&&a<=8207||a===65279)&&!(a>=768&&a<=879||a>=6832&&a<=6911||a>=7616&&a<=7679||a>=8400&&a<=8447||a>=65056&&a<=65071)&&!(a>=55296&&a<=57343)&&!(a>=65024&&a<=65039)&&!kS.test(o)){if((0,W1.default)().test(o)){i+=2;continue}i+=$1(a,u)}}return i}function Al({stream:e=process.stdout}={}){return!!(e&&e.isTTY&&process.env.TERM!=="dumb"&&!("CI"in process.env))}var Re=O(require("node:process"),1);function Ol(){return Re.default.platform!=="win32"?Re.default.env.TERM!=="linux":!!Re.default.env.WT_SESSION||!!Re.default.env.TERMINUS_SUBLIME||Re.default.env.ConEmuTask==="{cmd::Cmder}"||Re.default.env.TERM_PROGRAM==="Terminus-Sublime"||Re.default.env.TERM_PROGRAM==="vscode"||Re.default.env.TERM==="xterm-256color"||Re.default.env.TERM==="alacritty"||Re.default.env.TERMINAL_EMULATOR==="JetBrains-JediTerm"}var Le=O(require("node:process"),1),TS=3,Sl=class{#i=0;start(){this.#i++,this.#i===1&&this.#t()}stop(){if(this.#i<=0)throw new Error("`stop` called more times than `start`");this.#i--,this.#i===0&&this.#e()}#t(){Le.default.platform==="win32"||!Le.default.stdin.isTTY||(Le.default.stdin.setRawMode(!0),Le.default.stdin.on("data",this.#u),Le.default.stdin.resume())}#e(){Le.default.stdin.isTTY&&(Le.default.stdin.off("data",this.#u),Le.default.stdin.pause(),Le.default.stdin.setRawMode(!1))}#u(r){r[0]===TS&&Le.default.emit("SIGINT")}},MS=new Sl,Bl=MS;var IS=O(Ei(),1),ql=class{#i=0;#t=!1;#e=0;#u=0;#r;#o;#n;#x;#d;#c;#l;#f;#D;#a;#s;color;constructor(r){typeof r=="string"&&(r={text:r}),this.#r={color:"cyan",stream:ui.default.stderr,discardStdin:!0,hideCursor:!0,...r},this.color=this.#r.color,this.spinner=this.#r.spinner,this.#d=this.#r.interval,this.#n=this.#r.stream,this.#c=typeof this.#r.isEnabled=="boolean"?this.#r.isEnabled:Al({stream:this.#n}),this.#l=typeof this.#r.isSilent=="boolean"?this.#r.isSilent:!1,this.text=this.#r.text,this.prefixText=this.#r.prefixText,this.suffixText=this.#r.suffixText,this.indent=this.#r.indent,ui.default.env.NODE_ENV==="test"&&(this._stream=this.#n,this._isEnabled=this.#c,Object.defineProperty(this,"_linesToClear",{get(){return this.#i},set(t){this.#i=t}}),Object.defineProperty(this,"_frameIndex",{get(){return this.#u}}),Object.defineProperty(this,"_lineCount",{get(){return this.#e}}))}get indent(){return this.#f}set indent(r=0){if(!(r>=0&&Number.isInteger(r)))throw new Error("The `indent` option must be an integer from 0 and up");this.#f=r,this.#p()}get interval(){return this.#d??this.#o.interval??100}get spinner(){return this.#o}set spinner(r){if(this.#u=0,this.#d=void 0,typeof r=="object"){if(r.frames===void 0)throw new Error("The given spinner must have a `frames` property");this.#o=r}else if(!Ol())this.#o=oi.default.line;else if(r===void 0)this.#o=oi.default.dots;else if(r!=="default"&&oi.default[r])this.#o=oi.default[r];else throw new Error(`There is no built-in spinner named '${r}'. See https://github.com/sindresorhus/cli-spinners/blob/main/spinners.json for a full list.`)}get text(){return this.#D}set text(r=""){this.#D=r,this.#p()}get prefixText(){return this.#a}set prefixText(r=""){this.#a=r,this.#p()}get suffixText(){return this.#s}set suffixText(r=""){this.#s=r,this.#p()}get isSpinning(){return this.#x!==void 0}#h(r=this.#a,t=" "){return typeof r=="string"&&r!==""?r+t:typeof r=="function"?r()+t:""}#m(r=this.#s,t=" "){return typeof r=="string"&&r!==""?t+r:typeof r=="function"?t+r():""}#p(){let r=this.#n.columns??80,t=this.#h(this.#a,"-"),n=this.#m(this.#s,"-"),i=" ".repeat(this.#f)+t+"--"+this.#D+"--"+n;this.#e=0;for(let u of ii(i).split(`
62
62
  `))this.#e+=Math.max(1,Math.ceil(wl(u,{countAnsiEscapeCodes:!0})/r))}get isEnabled(){return this.#c&&!this.#l}set isEnabled(r){if(typeof r!="boolean")throw new TypeError("The `isEnabled` option must be a boolean");this.#c=r}get isSilent(){return this.#l}set isSilent(r){if(typeof r!="boolean")throw new TypeError("The `isSilent` option must be a boolean");this.#l=r}frame(){let{frames:r}=this.#o,t=r[this.#u];this.color&&(t=z[this.color](t)),this.#u=++this.#u%r.length;let n=typeof this.#a=="string"&&this.#a!==""?this.#a+" ":"",i=typeof this.text=="string"?" "+this.text:"",u=typeof this.#s=="string"&&this.#s!==""?" "+this.#s:"";return n+t+i+u}clear(){if(!this.#c||!this.#n.isTTY)return this;this.#n.cursorTo(0);for(let r=0;r<this.#i;r++)r>0&&this.#n.moveCursor(0,-1),this.#n.clearLine(1);return(this.#f||this.lastIndent!==this.#f)&&this.#n.cursorTo(this.#f),this.lastIndent=this.#f,this.#i=0,this}render(){return this.#l?this:(this.clear(),this.#n.write(this.frame()),this.#i=this.#e,this)}start(r){return r&&(this.text=r),this.#l?this:this.#c?this.isSpinning?this:(this.#r.hideCursor&&yl.hide(this.#n),this.#r.discardStdin&&ui.default.stdin.isTTY&&(this.#t=!0,Bl.start()),this.render(),this.#x=setInterval(this.render.bind(this),this.interval),this):(this.text&&this.#n.write(`- ${this.text}
63
63
  `),this)}stop(){return this.#c?(clearInterval(this.#x),this.#x=void 0,this.#u=0,this.clear(),this.#r.hideCursor&&yl.show(this.#n),this.#r.discardStdin&&ui.default.stdin.isTTY&&this.#t&&(Bl.stop(),this.#t=!1),this):this}succeed(r){return this.stopAndPersist({symbol:ni.success,text:r})}fail(r){return this.stopAndPersist({symbol:ni.error,text:r})}warn(r){return this.stopAndPersist({symbol:ni.warning,text:r})}info(r){return this.stopAndPersist({symbol:ni.info,text:r})}stopAndPersist(r={}){if(this.#l)return this;let t=r.prefixText??this.#a,n=this.#h(t," "),i=r.symbol??" ",u=r.text??this.text,o=typeof u=="string"?" "+u:"",a=r.suffixText??this.#s,s=this.#m(a," "),c=n+i+o+s+`
64
- `;return this.stop(),this.#n.write(c),this}};function _e(e){return new ql(e)}var z1=require("child_process"),Be=O(require("fs"),1),at=O(require("path"),1);var H1={dependencies:{"@gorhom/bottom-sheet":"5.2.8","@hookform/resolvers":"5.0.1","@react-native-community/datetimepicker":"8.6.0","@react-native-community/hooks":"100.1.0","@react-navigation/core":"7.4.0","@react-navigation/material-top-tabs":"7.2.13","@react-navigation/native-stack":"7.2.1","@react-navigation/native":"7.0.15","@react-navigation/stack":"7.1.2","@shopify/react-native-skia":"2.4.14","@tanstack/query-async-storage-persister":"5.67.3","@tanstack/query-core":"5.67.3","@tanstack/query-sync-storage-persister":"5.67.3","@tanstack/react-query-persist-client":"5.67.3","@tanstack/react-query":"5.67.3",axios:"1.13.2","babel-plugin-module-resolver":"5.0.2",dayjs:"1.11.19","jotai-optics":"0.4.0",jotai:"2.16.1","lodash-es":"4.17.22","react-hook-form":"7.56.4","react-native-dotenv":"3.4.11","react-native-gesture-handler":"2.29.1","react-native-get-random-values":"1.11.0","react-native-haptic-feedback":"2.3.3","react-native-keyboard-controller":"1.19.1","react-native-mmkv":"4.1.0","react-native-modal-datetime-picker":"18.0.0","react-native-modal":"14.0.0-rc.1","react-native-nitro-modules":"0.32.0","react-native-pager-view":"6.8.0","react-native-reanimated":"4.2.1","react-native-safe-area-context":"5.6.0","react-native-screens":"4.19.0","react-native-simple-toast":"3.3.2","react-native-turbo-image":"1.22.1","react-native-url-polyfill":"2.0.0","react-native-worklets":"0.7.1","react-native":"0.83.1",react:"19.2.0",tailwindcss:"3.4.16",twrnc:"4.16.0",uuid:"11.1.0","zod-validation-error":"3.4.0",zod:"3.24.2",zustand:"5.0.5"}};var G1={devDependencies:{"@babel/core":"7.25.2","@babel/eslint-parser":"7.23.10","@babel/preset-env":"7.25.3","@babel/preset-react":"7.27.1","@babel/runtime":"7.25.0","@react-native-community/cli-platform-android":"20.0.0","@react-native-community/cli-platform-ios":"20.0.0","@react-native-community/cli":"20.0.0","@react-native/babel-preset":"0.83.1","@react-native/eslint-config":"0.83.1","@react-native/metro-config":"0.83.1","@react-native/typescript-config":"0.83.1","@swc-node/register":"~1.8.0","@tanstack/eslint-plugin-query":"5.91.2","@testing-library/jest-native":"5.4.3","@testing-library/react-native":"12.5.1","@types/jest":"29.5.13","@types/jwt-encode":"1.0.3","@types/lodash-es":"4.17.12","@types/react-native-vector-icons":"6.4.18","@types/react-test-renderer":"19.1.0","@types/react":"19.2.0","@typescript-eslint/eslint-plugin":"^6.13.2","@typescript-eslint/parser":"^6.21.0","babel-jest":"29.6.3","babel-plugin-module-resolver":"5.0.2","eslint-config-airbnb-typescript":"^17.1.0","eslint-config-prettier":"10.1.8","eslint-import-resolver-typescript":"^3.8.5","eslint-plugin-ft-flow":"2.0.3","eslint-plugin-import":"^2.31.0","eslint-plugin-jest":"27.9.0","eslint-plugin-prettier":"^5.2.3","eslint-plugin-react-hooks":"^4.6.0","eslint-plugin-react-perf":"^3.3.3","eslint-plugin-react":"^7.32.2","eslint-plugin-sonarjs":"0.25.1","eslint-plugin-sort-destructure-keys":"^2.0.0","eslint-plugin-sort-keys-fix":"^1.1.2","eslint-plugin-tailwindcss":"^3.18.0","eslint-plugin-testing-library":"6.2.2",eslint:"8.57.0",husky:"9.1.7","jest-environment-jsdom":"30.1.2",jest:"29.6.3","prettier-plugin-tailwindcss":"^0.6.11",prettier:"3.3.2","react-native-dotenv":"3.4.11","react-native-svg-transformer":"1.3.0","react-native-svg":"15.15.1","react-test-renderer":"19.2.0",typescript:"5.8.3"}};function fa(){return __dirname}var pe=(e,r)=>{(0,z1.execSync)(`cd ${e} && ${r}`,{stdio:"inherit"})},te=(e,r,t,n)=>{let i=at.default.join(fa(),`../templates/${t}/${r}`);!Be.default.existsSync(i)&&n&&(i=at.default.join(fa(),`../templates/${n}/${r}`)),Be.default.copyFile(i,e,u=>{if(u){console.error(`Error copying file: ${u.message}`);return}})},Ne=(e,r,t,n)=>{let i=at.default.join(fa(),`../templates/${t}/${r}`);!Be.default.existsSync(i)&&n&&(i=at.default.join(fa(),`../templates/${n}/${r}`)),Be.default.cpSync(i,e,{recursive:!0},u=>{if(u){console.error(`Error copying file: ${u.message}`);return}})},jl=e=>{Be.default.rmSync(e,{recursive:!0,force:!0},r=>{if(r){console.error(`Error removing directory: ${r.message}`);return}})},ai=e=>{Be.default.rmSync(e,{recursive:!0,force:!0},r=>{if(r){console.error(`Error removing file: ${r.message}`);return}})},Pl=e=>{let r={doctor:"npx nx react-native doctor",android:"cd apps/mobile && npm run run-android","android:connect":"cd apps/mobile && npm run android:connect","check-env:mobile":"./check-env.sh apps/mobile/.env apps/mobile/.env.template",ios:"cd apps/mobile && npm run run-ios",clean:"./clean-generated-outputs.sh","create-env":"printenv > ","lint:all":"npx nx run-many -t lint -p mobile --parallel=1 --skip-nx-cache","lint:mobile":"npx nx run mobile:lint --skip-nx-cache","mobile-android":"cd apps/mobile && npm run run-android","mobile-ios":"cd apps/mobile && npm run run-ios",prepare:"husky install","serve:mobile":"cd apps/mobile && npm start","serve:all":"npx nx run-many -t serve -p mobile --parallel=1 --skip-nx-cache",xcode:"cd apps/mobile && npm run xcode","touch-xcode":"cd apps/mobile && npm run touch-xcode","setup-fastlane":"cd apps/mobile && npm run setup-fastlane","deploy-android:dev":"cd apps/mobile && npm run deploy-android:dev","deploy-ios:dev":"cd apps/mobile && npm run deploy-ios:dev","ios-certificates":"cd apps/mobile && npm run ios-certificates","pod-install":"cd apps/mobile && npm run pod-install","react-native-asset":"cd apps/mobile && npx react-native-asset"},t=at.default.join(e,"package.json"),n=JSON.parse(Be.default.readFileSync(t,"utf8"));n.dependencies={...n.dependencies,...H1.dependencies},n.devDependencies={...n.devDependencies,...G1.devDependencies},n.scripts={...n.scripts,...r},Be.default.writeFileSync(t,JSON.stringify(n,null,2))},K1=e=>{let r=at.default.join(e,"nx.json");if(!Be.default.existsSync(r))return;let t=JSON.parse(Be.default.readFileSync(r,"utf-8"));t.tui={...t.tui||{},enabled:!1},Be.default.writeFileSync(r,JSON.stringify(t,null,2))},kl=(e,r)=>{let t=_e({text:"Configuring iOS Dev scheme and build configurations...",color:"cyan"}).start();try{pe(e,"bundle check || bundle install"),pe(e,"bundle exec ruby ./scripts/setup-ios-dev-scheme.rb"),t.succeed(r.success("iOS Dev scheme and build configurations configured successfully"))}catch(n){throw t.fail(r.error("Failed to configure iOS Dev scheme and build configurations")),n}};var NS="2.6.2",De="21.2.2",E={title:z.bold.cyan,subtitle:z.cyan,success:z.bold.green,info:z.bold.blue,warning:z.hex("#FFA500").bold,error:z.bold.red,highlight:z.bold.magenta,command:z.yellow.italic,path:z.green.underline,step:e=>z.bgCyan.black(` STEP ${e} `),emoji:{rocket:"\u{1F680}",check:"\u2705",warning:"\u26A0\uFE0F",star:"\u2B50",sparkles:"\u2728",tools:"\u{1F6E0}\uFE0F",mobile:"\u{1F4F1}",folder:"\u{1F4C1}",code:"\u{1F4BB}"}},J1=()=>{console.log(`
64
+ `;return this.stop(),this.#n.write(c),this}};function _e(e){return new ql(e)}var z1=require("child_process"),Be=O(require("fs"),1),at=O(require("path"),1);var H1={dependencies:{"@gorhom/bottom-sheet":"5.2.8","@hookform/resolvers":"5.0.1","@react-native-community/datetimepicker":"8.6.0","@react-native-community/hooks":"100.1.0","@react-navigation/core":"7.4.0","@react-navigation/material-top-tabs":"7.2.13","@react-navigation/native-stack":"7.2.1","@react-navigation/native":"7.0.15","@react-navigation/stack":"7.1.2","@shopify/react-native-skia":"2.4.14","@tanstack/query-async-storage-persister":"5.67.3","@tanstack/query-core":"5.67.3","@tanstack/query-sync-storage-persister":"5.67.3","@tanstack/react-query-persist-client":"5.67.3","@tanstack/react-query":"5.67.3",axios:"1.13.2","babel-plugin-module-resolver":"5.0.2",dayjs:"1.11.19","jotai-optics":"0.4.0",jotai:"2.16.1","lodash-es":"4.17.22","react-hook-form":"7.56.4","react-native-dotenv":"3.4.11","react-native-gesture-handler":"2.29.1","react-native-get-random-values":"1.11.0","react-native-haptic-feedback":"2.3.3","react-native-keyboard-controller":"1.19.1","react-native-mmkv":"4.1.0","react-native-modal-datetime-picker":"18.0.0","react-native-modal":"14.0.0-rc.1","react-native-nitro-modules":"0.32.0","react-native-pager-view":"6.8.0","react-native-reanimated":"4.2.1","react-native-safe-area-context":"5.6.0","react-native-screens":"4.19.0","react-native-simple-toast":"3.3.2","react-native-turbo-image":"1.22.1","react-native-url-polyfill":"2.0.0","react-native-worklets":"0.7.1","react-native":"0.83.1",react:"19.2.0",tailwindcss:"3.4.16",twrnc:"4.16.0",uuid:"11.1.0","zod-validation-error":"3.4.0",zod:"3.24.2",zustand:"5.0.5"}};var G1={devDependencies:{"@babel/core":"7.25.2","@babel/eslint-parser":"7.23.10","@babel/preset-env":"7.25.3","@babel/preset-react":"7.27.1","@babel/runtime":"7.25.0","@react-native-community/cli-platform-android":"20.0.0","@react-native-community/cli-platform-ios":"20.0.0","@react-native-community/cli":"20.0.0","@react-native/babel-preset":"0.83.1","@react-native/eslint-config":"0.83.1","@react-native/metro-config":"0.83.1","@react-native/typescript-config":"0.83.1","@swc-node/register":"~1.8.0","@tanstack/eslint-plugin-query":"5.91.2","@testing-library/jest-native":"5.4.3","@testing-library/react-native":"12.5.1","@types/jest":"29.5.13","@types/jwt-encode":"1.0.3","@types/lodash-es":"4.17.12","@types/react-native-vector-icons":"6.4.18","@types/react-test-renderer":"19.1.0","@types/react":"19.2.0","@typescript-eslint/eslint-plugin":"^6.13.2","@typescript-eslint/parser":"^6.21.0","babel-jest":"29.6.3","babel-plugin-module-resolver":"5.0.2","eslint-config-airbnb-typescript":"^17.1.0","eslint-config-prettier":"10.1.8","eslint-import-resolver-typescript":"^3.8.5","eslint-plugin-ft-flow":"2.0.3","eslint-plugin-import":"^2.31.0","eslint-plugin-jest":"27.9.0","eslint-plugin-prettier":"^5.2.3","eslint-plugin-react-hooks":"^4.6.0","eslint-plugin-react-perf":"^3.3.3","eslint-plugin-react":"^7.32.2","eslint-plugin-sonarjs":"0.25.1","eslint-plugin-sort-destructure-keys":"^2.0.0","eslint-plugin-sort-keys-fix":"^1.1.2","eslint-plugin-tailwindcss":"^3.18.0","eslint-plugin-testing-library":"6.2.2",eslint:"8.57.0",husky:"9.1.7","jest-environment-jsdom":"30.1.2",jest:"29.6.3","prettier-plugin-tailwindcss":"^0.6.11",prettier:"3.3.2","react-native-dotenv":"3.4.11","react-native-svg-transformer":"1.3.0","react-native-svg":"15.15.1","react-test-renderer":"19.2.0",typescript:"5.8.3"}};function fa(){return __dirname}var pe=(e,r)=>{(0,z1.execSync)(`cd ${e} && ${r}`,{stdio:"inherit"})},te=(e,r,t,n)=>{let i=at.default.join(fa(),`../templates/${t}/${r}`);!Be.default.existsSync(i)&&n&&(i=at.default.join(fa(),`../templates/${n}/${r}`)),Be.default.copyFile(i,e,u=>{if(u){console.error(`Error copying file: ${u.message}`);return}})},Ne=(e,r,t,n)=>{let i=at.default.join(fa(),`../templates/${t}/${r}`);!Be.default.existsSync(i)&&n&&(i=at.default.join(fa(),`../templates/${n}/${r}`)),Be.default.cpSync(i,e,{recursive:!0},u=>{if(u){console.error(`Error copying file: ${u.message}`);return}})},jl=e=>{Be.default.rmSync(e,{recursive:!0,force:!0},r=>{if(r){console.error(`Error removing directory: ${r.message}`);return}})},ai=e=>{Be.default.rmSync(e,{recursive:!0,force:!0},r=>{if(r){console.error(`Error removing file: ${r.message}`);return}})},Pl=e=>{let r={doctor:"npx nx react-native doctor",android:"cd apps/mobile && npm run run-android","android:connect":"cd apps/mobile && npm run android:connect","check-env:mobile":"./check-env.sh apps/mobile/.env apps/mobile/.env.template",ios:"cd apps/mobile && npm run run-ios",clean:"./clean-generated-outputs.sh","create-env":"printenv > ","lint:all":"npx nx run-many -t lint -p mobile --parallel=1 --skip-nx-cache","lint:mobile":"npx nx run mobile:lint --skip-nx-cache","mobile-android":"cd apps/mobile && npm run run-android","mobile-ios":"cd apps/mobile && npm run run-ios",prepare:"husky install","serve:mobile":"cd apps/mobile && npm start","serve:all":"npx nx run-many -t serve -p mobile --parallel=1 --skip-nx-cache",xcode:"cd apps/mobile && npm run xcode","touch-xcode":"cd apps/mobile && npm run touch-xcode","setup-fastlane":"cd apps/mobile && npm run setup-fastlane","deploy-android:dev":"cd apps/mobile && npm run deploy-android:dev","deploy-ios:dev":"cd apps/mobile && npm run deploy-ios:dev","ios-certificates":"cd apps/mobile && npm run ios-certificates","pod-install":"cd apps/mobile && npm run pod-install","react-native-asset":"cd apps/mobile && npx react-native-asset"},t=at.default.join(e,"package.json"),n=JSON.parse(Be.default.readFileSync(t,"utf8"));n.dependencies={...n.dependencies,...H1.dependencies},n.devDependencies={...n.devDependencies,...G1.devDependencies},n.scripts={...n.scripts,...r},Be.default.writeFileSync(t,JSON.stringify(n,null,2))},K1=e=>{let r=at.default.join(e,"nx.json");if(!Be.default.existsSync(r))return;let t=JSON.parse(Be.default.readFileSync(r,"utf-8"));t.tui={...t.tui||{},enabled:!1},Be.default.writeFileSync(r,JSON.stringify(t,null,2))},kl=(e,r)=>{let t=_e({text:"Configuring iOS Dev scheme and build configurations...",color:"cyan"}).start();try{pe(e,"bundle check || bundle install"),pe(e,"bundle exec ruby ./scripts/setup-ios-dev-scheme.rb"),t.succeed(r.success("iOS Dev scheme and build configurations configured successfully"))}catch(n){throw t.fail(r.error("Failed to configure iOS Dev scheme and build configurations")),n}};var NS="2.6.4",De="21.2.2",E={title:z.bold.cyan,subtitle:z.cyan,success:z.bold.green,info:z.bold.blue,warning:z.hex("#FFA500").bold,error:z.bold.red,highlight:z.bold.magenta,command:z.yellow.italic,path:z.green.underline,step:e=>z.bgCyan.black(` STEP ${e} `),emoji:{rocket:"\u{1F680}",check:"\u2705",warning:"\u26A0\uFE0F",star:"\u2B50",sparkles:"\u2728",tools:"\u{1F6E0}\uFE0F",mobile:"\u{1F4F1}",folder:"\u{1F4C1}",code:"\u{1F4BB}"}},J1=()=>{console.log(`
65
65
  `),console.log(E.title("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")),console.log(E.title("\u2551 \u2551")),console.log(E.title("\u2551 ")+E.highlight("NX REACT NATIVE CLI")+E.title(" \u2551")),console.log(E.title("\u2551 ")+E.subtitle("A powerful starter for React Native with NX")+E.title(" \u2551")),console.log(E.title("\u2551 \u2551")),console.log(E.title("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D")),console.log(`
66
66
  `)};yn.name("React Native Starter with NX").description("A starter script to create a new React Native project with NX").version(NS);yn.command("create [workspace_name] [bundle_id]").description("create nx workspace with react-native").option("--fresh","Create a fresh project without copying template files").option("--nx-version <version>","Specify Nx version to use",De).option("--package-manager <pm>","Package manager to use (yarn or npm)","yarn").option("--skip-install","Skip package install after adding dependencies").option("--skip-configs","Skip copying prettier, eslint, and husky configs").action(async(e,r,t)=>{J1(),e||(e=(await ri.prompt([{type:"input",name:"workspace_name",message:E.info("Enter the workspace name:")}])).workspace_name),r||(r=(await ri.prompt([{type:"input",name:"bundle_id",message:E.info("Enter the bundle ID: ex. org.reactjsnative.example")}])).bundle_id);let n=t.fresh||!1,i=t.nxVersion||De,u=t.packageManager||"yarn",o=t.skipInstall||!1,a=t.skipConfigs||!1,s=process.cwd(),c=`${s}/${e}`,x=`${c}/apps/mobile`;console.log(`
67
67
  ${E.step(1)} ${E.emoji.folder} ${E.success(`Creating Nx workspace in ${E.path(`./${e}`)}`)}`);let d=_e({text:"Setting up Nx workspace...",color:"cyan"}).start();(0,Y1.execSync)(`cd ${s} && npx create-nx-workspace@${i} --preset apps --workspaceType integrated --name ${e} --package-manager=${u} --interactive false --nxCloud skip`,{stdio:"inherit"}),K1(c),d.succeed(E.success("Nx workspace created successfully")),console.log(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nx-react-native-cli",
3
- "version": "2.6.2",
3
+ "version": "2.6.4",
4
4
  "description": "A react native starter (with NX) cli script",
5
5
  "type": "module",
6
6
  "files": [
@@ -105,6 +105,8 @@ buck-out/
105
105
  **/fastlane/Preview.html
106
106
  **/fastlane/screenshots
107
107
  **/fastlane/test_output
108
+ **/fastlane/firebase-service-account.json
109
+
108
110
  *.cer
109
111
  *.mobileprovision
110
112
 
@@ -45,7 +45,7 @@ react {
45
45
 
46
46
  /* Hermes Commands */
47
47
  // The hermes compiler command to run. By default it is 'hermesc'
48
- // hermesCommand = "../../../../node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc"
48
+ hermesCommand = "../../node_modules/hermes-compiler/hermesc/%OS-BIN%/hermesc"
49
49
  //
50
50
  // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
51
51
  // hermesFlags = ["-O", "-output-source-map"]
@@ -105,6 +105,8 @@ buck-out/
105
105
  **/fastlane/Preview.html
106
106
  **/fastlane/screenshots
107
107
  **/fastlane/test_output
108
+ **/fastlane/firebase-service-account.json
109
+
108
110
  *.cer
109
111
  *.mobileprovision
110
112
 
@@ -45,7 +45,7 @@ react {
45
45
 
46
46
  /* Hermes Commands */
47
47
  // The hermes compiler command to run. By default it is 'hermesc'
48
- // hermesCommand = "../../../../node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc"
48
+ hermesCommand = "../../node_modules/hermes-compiler/hermesc/%OS-BIN%/hermesc"
49
49
  //
50
50
  // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
51
51
  // hermesFlags = ["-O", "-output-source-map"]
@@ -104,6 +104,8 @@ buck-out/
104
104
  **/fastlane/Preview.html
105
105
  **/fastlane/screenshots
106
106
  **/fastlane/test_output
107
+ **/fastlane/firebase-service-account.json
108
+
107
109
  *.cer
108
110
  *.mobileprovision
109
111
 
@@ -0,0 +1,118 @@
1
+ # generate_fastlane_env.sh
2
+
3
+ This document explains how to use `./generate_fastlane_env.sh` in `apps/mobile`.
4
+
5
+ ## What It Does
6
+
7
+ `generate_fastlane_env.sh` prepares Fastlane and Firebase project configuration by:
8
+
9
+ 1. Validating required tools and environment variables.
10
+ 2. Ensuring Firebase Android and iOS apps exist (creates if missing).
11
+ 3. Running Android signing report and registering `devDebug` signing hashes in Firebase:
12
+ - `SHA1`
13
+ - `SHA-256`
14
+ 4. Downloading Firebase app config files:
15
+ - Android: `android/app/src/dev/google-services.json` (for `development`)
16
+ - Android: `android/app/src/production/google-services.json` (for `production`)
17
+ - iOS: `ios/Firebase/Dev/GoogleService-Info.plist` (for `development`)
18
+ - iOS: `ios/Firebase/Prod/GoogleService-Info.plist` (for `production`)
19
+ 5. Best-effort service account setup for Firebase App Distribution.
20
+ 6. Rendering `fastlane/.env.<ENVIRONMENT>` from `fastlane/.env.template`.
21
+
22
+ ## Prerequisites
23
+
24
+ Install and authenticate:
25
+
26
+ - `firebase` CLI
27
+ - `gcloud` CLI
28
+ - `jq`
29
+ - `base64`
30
+
31
+ Auth requirements:
32
+
33
+ - Active `gcloud` account (`gcloud auth login`)
34
+ - Active Firebase login (`firebase login`)
35
+ - Access to `GCP_PROJECT_ID`
36
+
37
+ ## Required Environment Variables
38
+
39
+ These are required input variables (directly in shell env or via `--env-file`):
40
+
41
+ - `ENVIRONMENT` (`development`, `staging`, or `production`)
42
+ - `GCP_PROJECT_ID`
43
+ - `FASTLANE_APP_IDENTIFIER`
44
+ - `MATCH_PASSWORD`
45
+ - `MATCH_GIT_URL`
46
+ - `FASTLANE_APPSTORE_KEY_ID`
47
+ - `FASTLANE_APPSTORE_ISSUER_ID`
48
+ - `FASTLANE_APPSTORE_KEY_CONTENT`
49
+
50
+ Notes:
51
+
52
+ - `ENVIRONMENT` controls output env filename: `fastlane/.env.<ENVIRONMENT>`.
53
+ - Script input uses `MATCH_GIT_URL`, but generated Fastlane env still writes `GIT_URL=`.
54
+
55
+ ## Optional Variables
56
+
57
+ Common optional variables:
58
+
59
+ - `ANDROID_APPLICATION_ID` (defaults to `FASTLANE_APP_IDENTIFIER`)
60
+ - `FIREBASE_APP_NAME_ANDROID`
61
+ - `FIREBASE_APP_NAME_IOS`
62
+ - `FASTLANE_APP_IDENTIFIERS` (defaults to `FASTLANE_APP_IDENTIFIER`)
63
+ - `IS_CI` (defaults to `true`)
64
+ - `GIT_BRANCH` (defaults to `main`)
65
+ - `KEY_ALIAS` (defaults to `production`)
66
+
67
+ ## Usage
68
+
69
+ Recommended approach:
70
+
71
+ 1. Create `fastlane.env` in `apps/mobile` with all required variables.
72
+ 2. Run the script with `ENVFILE=fastlane.env`.
73
+
74
+ ```bash
75
+ ENVFILE=fastlane.env ./generate_fastlane_env.sh
76
+ ```
77
+
78
+ Run from `apps/mobile`:
79
+
80
+ ```bash
81
+ ./generate_fastlane_env.sh --env-file fastlane.env
82
+ ```
83
+
84
+ Or load from shell env:
85
+
86
+ ```bash
87
+ ENVIRONMENT=development \
88
+ GCP_PROJECT_ID=your-project-id \
89
+ FASTLANE_APP_IDENTIFIER=com.example.app.dev \
90
+ MATCH_PASSWORD=*** \
91
+ MATCH_GIT_URL=git@github.com:org/certs.git \
92
+ FASTLANE_APPSTORE_KEY_ID=*** \
93
+ FASTLANE_APPSTORE_ISSUER_ID=*** \
94
+ FASTLANE_APPSTORE_KEY_CONTENT="..." \
95
+ ./generate_fastlane_env.sh
96
+ ```
97
+
98
+ Non-interactive account confirmation:
99
+
100
+ ```bash
101
+ ./generate_fastlane_env.sh --env-file fastlane.env --yes
102
+ ```
103
+
104
+ ## Output Files
105
+
106
+ Generated/updated by the script:
107
+
108
+ - `fastlane/.env.<ENVIRONMENT>`
109
+ - `android/app/src/dev/google-services.json` (when `ENVIRONMENT=development`)
110
+ - `android/app/src/production/google-services.json` (when `ENVIRONMENT=production`)
111
+ - `ios/Firebase/Dev/GoogleService-Info.plist` (when `ENVIRONMENT=development`)
112
+ - `ios/Firebase/Prod/GoogleService-Info.plist` (when `ENVIRONMENT=production`)
113
+ - `fastlane/firebase-service-account.json` (best effort)
114
+
115
+ ## Behavior Notes
116
+
117
+ - Service account creation/binding/key generation is best effort and will not fail the whole script.
118
+ - For `ENVIRONMENT=staging`, Firebase config file download is currently skipped for Android/iOS path placement.
@@ -249,7 +249,7 @@ def publish_bundle(flavor)
249
249
  UI.message("Uploading aab build to Firebase...")
250
250
  release = firebase_app_distribution(
251
251
  service_credentials_file: service_account_path,
252
- app: ENV['FASTLANE_FIREBASE_APP_ID'],
252
+ app: ENV['FASTLANE_FIREBASE_APP_ID_ANDROID'],
253
253
  android_artifact_type: "AAB",
254
254
  android_artifact_path: "./android/app/build/outputs/bundle/#{flavor}Release/app-#{flavor}-release.aab",
255
255
  release_notes: ENV["FASTLANE_CHANGELOGS"],
@@ -261,7 +261,7 @@ def publish_apk(flavor)
261
261
  UI.message("Uploading apk build to Firebase...")
262
262
  release = firebase_app_distribution(
263
263
  service_credentials_file: service_account_path,
264
- app: ENV['FASTLANE_FIREBASE_APP_ID'],
264
+ app: ENV['FASTLANE_FIREBASE_APP_ID_ANDROID'],
265
265
  android_artifact_type: "APK",
266
266
  android_artifact_path: "./android/app/build/outputs/apk/#{flavor}/release/app-#{flavor}-release.apk",
267
267
  release_notes: ENV["FASTLANE_CHANGELOGS"],
@@ -0,0 +1,615 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Generates a fastlane env file from a template by:
5
+ # 1) validating prerequisites/auth/env vars
6
+ # 2) creating/reusing Firebase apps
7
+ # 3) registering Android devDebug SHA1 on Firebase
8
+ # 4) downloading Android/iOS Firebase config files for selected environments
9
+ # 5) creating/reusing a GCP service account + key
10
+ # 6) writing deterministic key/value output to the target env file
11
+
12
+ SCRIPT_NAME="$(basename "$0")"
13
+
14
+ TEMPLATE_PATH="fastlane/.env.template"
15
+ OUTPUT_PATH="fastlane/.env"
16
+ ENV_FILE_PATH="${ENVFILE:-}"
17
+ SERVICE_ACCOUNT_KEY_PATH="fastlane/firebase-service-account.json"
18
+ SERVICE_ACCOUNT_NAME="fad-admin"
19
+ FIREBASE_APPDIST_ROLE="roles/firebaseappdistro.admin"
20
+ AUTO_APPROVE=0
21
+ OUTPUT_PATH_EXPLICIT=0
22
+
23
+ usage() {
24
+ cat <<USAGE
25
+ Usage: $SCRIPT_NAME [options]
26
+
27
+ Generate fastlane/.env from fastlane/.env.template by reading env vars and ensuring
28
+ Firebase/GCP resources exist.
29
+
30
+ Options:
31
+ --env-file <path> Load input variables from dotenv file (same as ENVFILE=<path>)
32
+ --template <path> Template file path (default: fastlane/.env.template)
33
+ --output <path> Output env file path (default: fastlane/.env)
34
+ --service-account-key <path> Service account JSON key path (default: fastlane/firebase-service-account.json)
35
+ --service-account-name <name> Service account name prefix (default: fad-admin)
36
+ --appdist-role <role> IAM role for Firebase App Distribution (default: roles/firebaseappdistro.admin)
37
+ --yes Skip interactive account confirmation prompt
38
+ -h, --help Show this help
39
+ USAGE
40
+ }
41
+
42
+ log() { printf '[%s] %s\n' "$SCRIPT_NAME" "$*" >&2; }
43
+ warn() { printf '[%s] WARNING: %s\n' "$SCRIPT_NAME" "$*" >&2; }
44
+ die() { printf '[%s] ERROR: %s\n' "$SCRIPT_NAME" "$*" >&2; exit 1; }
45
+
46
+ command_exists() { command -v "$1" >/dev/null 2>&1; }
47
+
48
+ require_command() {
49
+ local cmd="$1"
50
+ command_exists "$cmd" || die "Required command not found: $cmd"
51
+ }
52
+
53
+ require_env() {
54
+ local name="$1"
55
+ if [[ -z "${!name:-}" ]]; then
56
+ die "Required environment variable is missing: $name"
57
+ fi
58
+ }
59
+
60
+ load_env_file_if_configured() {
61
+ if [[ -z "$ENV_FILE_PATH" ]]; then
62
+ return
63
+ fi
64
+
65
+ [[ -f "$ENV_FILE_PATH" ]] || die "Env file not found: $ENV_FILE_PATH"
66
+ log "Loading variables from $ENV_FILE_PATH"
67
+ # shellcheck disable=SC1090
68
+ set -a
69
+ source "$ENV_FILE_PATH"
70
+ set +a
71
+ }
72
+
73
+ has_gcp_project_visibility() {
74
+ local project_id="$1"
75
+
76
+ # Prefer direct describe when allowed.
77
+ if gcloud projects describe "$project_id" --format='value(projectId)' >/dev/null 2>&1; then
78
+ return 0
79
+ fi
80
+
81
+ # Fallback for identities that cannot call describe but can still list projects.
82
+ local listed_id=''
83
+ listed_id="$(gcloud projects list --filter="projectId=$project_id" --format='value(projectId)' 2>/dev/null | head -n1 || true)"
84
+ [[ "$listed_id" == "$project_id" ]]
85
+ }
86
+
87
+ require_firebase_project_access() {
88
+ local project_id="$1"
89
+ local projects_json=''
90
+
91
+ # Verifies the current Firebase auth can access this project.
92
+ projects_json="$(firebase projects:list --json --non-interactive)"
93
+ if ! jq -e --arg pid "$project_id" '[.. | objects | .projectId? // empty] | index($pid) != null' <<<"$projects_json" >/dev/null; then
94
+ die "Firebase account cannot access project: $project_id"
95
+ fi
96
+ }
97
+
98
+ get_active_gcloud_account() {
99
+ gcloud auth list --filter=status:ACTIVE --format='value(account)' 2>/dev/null | head -n1
100
+ }
101
+
102
+ get_active_firebase_account() {
103
+ # login:list is the least ambiguous source for current Firebase CLI identity.
104
+ firebase login:list --json --non-interactive 2>/dev/null \
105
+ | jq -r '[.. | objects | .email? // empty][0] // empty' \
106
+ | head -n1
107
+ }
108
+
109
+ get_base64_file() {
110
+ local file_path="$1"
111
+ # Keep the value on one line to fit dotenv usage and Fastfile decoding.
112
+ base64 < "$file_path" | tr -d '\n'
113
+ }
114
+
115
+ dotenv_escape() {
116
+ local raw="$1"
117
+ if [[ -z "$raw" ]]; then
118
+ printf ''
119
+ return
120
+ fi
121
+
122
+ # Keep unquoted when safe; quote and escape otherwise.
123
+ if [[ "$raw" =~ ^[A-Za-z0-9_./:@,+=-]+$ ]]; then
124
+ printf '%s' "$raw"
125
+ return
126
+ fi
127
+
128
+ local escaped="$raw"
129
+ escaped=${escaped//\\/\\\\}
130
+ escaped=${escaped//\"/\\\"}
131
+ escaped=${escaped//\$/\\$}
132
+ printf '"%s"' "$escaped"
133
+ }
134
+
135
+ extract_first_app_id_from_json() {
136
+ local json="$1"
137
+ local selector="$2"
138
+ # Firebase CLI JSON shape can vary, so traverse recursively and match by identifier.
139
+ jq -r --arg selector "$selector" '
140
+ [
141
+ .. | objects |
142
+ select(.appId? != null) |
143
+ select((.packageName? == $selector) or (.bundleId? == $selector)) |
144
+ .appId
145
+ ][0] // empty
146
+ ' <<<"$json"
147
+ }
148
+
149
+ extract_first_app_id_from_create_json() {
150
+ local json="$1"
151
+ # Read appId from create response regardless of nesting shape.
152
+ jq -r '[.. | objects | .appId? // empty][0] // empty' <<<"$json"
153
+ }
154
+
155
+ ensure_firebase_app() {
156
+ local platform="$1"
157
+ local identifier="$2"
158
+ local display_name="$3"
159
+ local app_id=''
160
+ local list_json=''
161
+
162
+ # First try to find an existing app for idempotent behavior.
163
+ list_json="$(firebase apps:list "$platform" --project "$GCP_PROJECT_ID" --json --non-interactive)"
164
+ app_id="$(extract_first_app_id_from_json "$list_json" "$identifier")"
165
+
166
+ if [[ -n "$app_id" ]]; then
167
+ log "Reusing existing Firebase $platform app for identifier $identifier"
168
+ printf '%s' "$app_id"
169
+ return
170
+ fi
171
+
172
+ # Create only when no matching app exists.
173
+ log "Creating Firebase $platform app for identifier $identifier"
174
+ local create_json=''
175
+ if [[ "$platform" == "ANDROID" ]]; then
176
+ create_json="$(firebase apps:create ANDROID "$display_name" --package-name "$identifier" --project "$GCP_PROJECT_ID" --json --non-interactive)"
177
+ elif [[ "$platform" == "IOS" ]]; then
178
+ create_json="$(firebase apps:create IOS "$display_name" --bundle-id "$identifier" --project "$GCP_PROJECT_ID" --json --non-interactive)"
179
+ else
180
+ die "Unsupported Firebase platform: $platform"
181
+ fi
182
+
183
+ app_id="$(extract_first_app_id_from_create_json "$create_json")"
184
+ [[ -n "$app_id" ]] || die "Failed to detect appId after creating Firebase $platform app"
185
+ printf '%s' "$app_id"
186
+ }
187
+
188
+ extract_devdebug_hash_from_signing_report() {
189
+ local report="$1"
190
+ local hash_label="$2"
191
+ awk -v label="$hash_label" '
192
+ /^Variant: / {
193
+ in_variant = ($2 == "devDebug")
194
+ next
195
+ }
196
+ in_variant && index($0, label ":") == 1 {
197
+ sub("^" label ":[[:space:]]*", "", $0)
198
+ print
199
+ exit
200
+ }
201
+ ' <<<"$report"
202
+ }
203
+
204
+ get_android_devdebug_hashes() {
205
+ local signing_report=''
206
+ local sha1=''
207
+ local sha256=''
208
+ local gradlew_path="android/gradlew"
209
+
210
+ [[ -x "$gradlew_path" ]] || die "Android gradle wrapper not found or not executable: $gradlew_path"
211
+
212
+ log "Running Android signingReport to read devDebug signing hashes"
213
+ signing_report="$(cd android && ./gradlew signingReport --console=plain)"
214
+ sha1="$(extract_devdebug_hash_from_signing_report "$signing_report" "SHA1")"
215
+ sha256="$(extract_devdebug_hash_from_signing_report "$signing_report" "SHA-256")"
216
+ [[ -n "$sha1" ]] || die "Could not find SHA1 for Variant: devDebug in Android signingReport output"
217
+ [[ -n "$sha256" ]] || die "Could not find SHA-256 for Variant: devDebug in Android signingReport output"
218
+ printf '%s\n%s' "$sha1" "$sha256"
219
+ }
220
+
221
+ ensure_firebase_android_hash() {
222
+ local app_id="$1"
223
+ local hash_value="$2"
224
+ local hash_name="$3"
225
+ local list_json=''
226
+
227
+ list_json="$(firebase apps:android:sha:list "$app_id" --project "$GCP_PROJECT_ID" --json --non-interactive 2>/dev/null || true)"
228
+ if [[ -n "$list_json" ]] && jq -e --arg sha "$hash_value" '[.. | objects | .shaHash? // empty] | index($sha) != null' <<<"$list_json" >/dev/null; then
229
+ log "Reusing existing Firebase Android $hash_name for app $app_id"
230
+ return
231
+ fi
232
+
233
+ log "Registering devDebug $hash_name on Firebase Android app $app_id"
234
+ if ! firebase apps:android:sha:create "$app_id" "$hash_value" --project "$GCP_PROJECT_ID" --json --non-interactive >/dev/null 2>&1; then
235
+ # Handle eventual consistency/races where another process created the SHA after list.
236
+ list_json="$(firebase apps:android:sha:list "$app_id" --project "$GCP_PROJECT_ID" --json --non-interactive 2>/dev/null || true)"
237
+ if [[ -n "$list_json" ]] && jq -e --arg sha "$hash_value" '[.. | objects | .shaHash? // empty] | index($sha) != null' <<<"$list_json" >/dev/null; then
238
+ log "Firebase Android $hash_name already exists after retry check; continuing"
239
+ return
240
+ fi
241
+ die "Failed to register $hash_name $hash_value on Firebase Android app $app_id"
242
+ fi
243
+ }
244
+
245
+ download_android_google_services_json() {
246
+ local app_id="$1"
247
+ local destination_dir=''
248
+ local destination_file=''
249
+ local tmp_file=''
250
+
251
+ case "$ENVIRONMENT" in
252
+ development)
253
+ destination_dir="android/app/src/dev"
254
+ ;;
255
+ production)
256
+ destination_dir="android/app/src/production"
257
+ ;;
258
+ *)
259
+ warn "Skipping google-services.json download for ENVIRONMENT=$ENVIRONMENT (supported by this step: development, production)"
260
+ return 0
261
+ ;;
262
+ esac
263
+
264
+ destination_file="${destination_dir}/google-services.json"
265
+ mkdir -p "$destination_dir"
266
+
267
+ tmp_file="$(mktemp)"
268
+ if ! firebase apps:sdkconfig ANDROID "$app_id" --project "$GCP_PROJECT_ID" --non-interactive >"$tmp_file"; then
269
+ rm -f "$tmp_file"
270
+ warn "Failed to download Android google-services.json for app $app_id"
271
+ return 1
272
+ fi
273
+
274
+ if [[ ! -s "$tmp_file" ]]; then
275
+ rm -f "$tmp_file"
276
+ warn "Downloaded Android google-services.json was empty for app $app_id"
277
+ return 1
278
+ fi
279
+
280
+ mv "$tmp_file" "$destination_file"
281
+ chmod 600 "$destination_file"
282
+ log "Wrote $destination_file"
283
+ }
284
+
285
+ download_ios_google_service_info_plist() {
286
+ local app_id="$1"
287
+ local destination_dir=''
288
+ local destination_file=''
289
+ local tmp_file=''
290
+
291
+ case "$ENVIRONMENT" in
292
+ development)
293
+ destination_dir="ios/Firebase/Dev"
294
+ ;;
295
+ production)
296
+ destination_dir="ios/Firebase/Prod"
297
+ ;;
298
+ *)
299
+ warn "Skipping iOS GoogleService-Info.plist download for ENVIRONMENT=$ENVIRONMENT (supported by this step: development, production)"
300
+ return 0
301
+ ;;
302
+ esac
303
+
304
+ destination_file="${destination_dir}/GoogleService-Info.plist"
305
+ mkdir -p "$destination_dir"
306
+
307
+ tmp_file="$(mktemp)"
308
+ if ! firebase apps:sdkconfig IOS "$app_id" --project "$GCP_PROJECT_ID" --non-interactive >"$tmp_file"; then
309
+ rm -f "$tmp_file"
310
+ warn "Failed to download iOS GoogleService-Info.plist for app $app_id"
311
+ return 1
312
+ fi
313
+
314
+ if [[ ! -s "$tmp_file" ]]; then
315
+ rm -f "$tmp_file"
316
+ warn "Downloaded iOS GoogleService-Info.plist was empty for app $app_id"
317
+ return 1
318
+ fi
319
+
320
+ mv "$tmp_file" "$destination_file"
321
+ chmod 600 "$destination_file"
322
+ log "Wrote $destination_file"
323
+ }
324
+
325
+ ensure_service_account() {
326
+ local sa_email="$1"
327
+
328
+ # Create once, then reuse across runs.
329
+ if gcloud iam service-accounts describe "$sa_email" --project "$GCP_PROJECT_ID" >/dev/null 2>&1; then
330
+ log "Reusing existing service account: $sa_email"
331
+ else
332
+ log "Creating service account: $sa_email"
333
+ if ! gcloud iam service-accounts create "$SERVICE_ACCOUNT_NAME" \
334
+ --project "$GCP_PROJECT_ID" \
335
+ --display-name "Firebase App Distribution Service Account" \
336
+ --description "Used by Fastlane Firebase App Distribution" \
337
+ --quiet >/dev/null 2>&1; then
338
+ return 1
339
+ fi
340
+ fi
341
+
342
+ # Safe to call repeatedly; command is effectively idempotent for existing binding.
343
+ if ! gcloud projects add-iam-policy-binding "$GCP_PROJECT_ID" \
344
+ --member "serviceAccount:$sa_email" \
345
+ --role "$FIREBASE_APPDIST_ROLE" \
346
+ --quiet >/dev/null 2>&1; then
347
+ return 1
348
+ fi
349
+
350
+ return 0
351
+ }
352
+
353
+ ensure_service_account_key() {
354
+ local sa_email="$1"
355
+ local key_path="$2"
356
+
357
+ mkdir -p "$(dirname "$key_path")"
358
+
359
+ # Keys should be generated once and then reused (avoid key sprawl).
360
+ if [[ -s "$key_path" ]]; then
361
+ log "Reusing existing service account key file at $key_path"
362
+ return
363
+ fi
364
+
365
+ log "Creating new service account key at $key_path"
366
+ umask 077
367
+ if ! gcloud iam service-accounts keys create "$key_path" \
368
+ --iam-account "$sa_email" \
369
+ --project "$GCP_PROJECT_ID" \
370
+ --quiet >/dev/null 2>&1; then
371
+ return 1
372
+ fi
373
+
374
+ return 0
375
+ }
376
+
377
+ preflight_checks() {
378
+ # Validate tools, template, required env, and auth access early.
379
+ require_command firebase
380
+ require_command gcloud
381
+ require_command jq
382
+ require_command base64
383
+
384
+ [[ -f "$TEMPLATE_PATH" ]] || die "Template not found: $TEMPLATE_PATH"
385
+
386
+ require_env GCP_PROJECT_ID
387
+ require_env FASTLANE_APP_IDENTIFIER
388
+ require_env MATCH_PASSWORD
389
+ require_env MATCH_GIT_URL
390
+ require_env FASTLANE_APPSTORE_KEY_ID
391
+ require_env FASTLANE_APPSTORE_ISSUER_ID
392
+ require_env FASTLANE_APPSTORE_KEY_CONTENT
393
+ require_env ENVIRONMENT
394
+
395
+ local active_account
396
+ active_account="$(get_active_gcloud_account || true)"
397
+ [[ -n "$active_account" ]] || die "No active gcloud account found. Run: gcloud auth login"
398
+
399
+ local current_project
400
+ current_project="$(gcloud config get-value project 2>/dev/null || true)"
401
+ if [[ -z "$current_project" || "$current_project" == "(unset)" ]]; then
402
+ warn "No default gcloud project configured. Commands will use GCP_PROJECT_ID=$GCP_PROJECT_ID explicitly."
403
+ fi
404
+
405
+ # Firebase confirms project existence in this workflow.
406
+ require_firebase_project_access "$GCP_PROJECT_ID"
407
+
408
+ # gcloud visibility checks can be stricter in some IAM setups; warn and continue.
409
+ if ! has_gcp_project_visibility "$GCP_PROJECT_ID"; then
410
+ warn "gcloud cannot verify visibility for project $GCP_PROJECT_ID. Proceeding because Firebase access is confirmed; IAM steps may still fail if permissions are insufficient."
411
+ fi
412
+ }
413
+
414
+ confirm_accounts() {
415
+ local gcloud_account="$1"
416
+ local firebase_account="$2"
417
+
418
+ if [[ "$AUTO_APPROVE" -eq 1 ]]; then
419
+ return
420
+ fi
421
+
422
+ if [[ ! -t 0 ]]; then
423
+ die "Interactive confirmation required but no TTY detected. Re-run with --yes."
424
+ fi
425
+
426
+ log "gcloud account: ${gcloud_account:-<unknown>}"
427
+ log "firebase account: ${firebase_account:-<unknown>}"
428
+
429
+ local reply=''
430
+ read -r -p "Proceed with these accounts? [y/N] " reply
431
+ case "$reply" in
432
+ y|Y|yes|YES)
433
+ ;;
434
+ *)
435
+ die "Aborted by user."
436
+ ;;
437
+ esac
438
+ }
439
+
440
+ render_env_file() {
441
+ local android_app_id="$1"
442
+ local ios_app_id="$2"
443
+ local firebase_service_account_b64="$3"
444
+ local is_ci="${IS_CI:-true}"
445
+
446
+ local git_branch="${GIT_BRANCH:-main}"
447
+ local app_identifiers="${FASTLANE_APP_IDENTIFIERS:-${FASTLANE_APP_IDENTIFIER}}"
448
+
449
+ # Backward compatibility for templates that still have a single Firebase app id variable.
450
+ local legacy_firebase_app_id="${FASTLANE_FIREBASE_APP_ID:-$android_app_id}"
451
+
452
+ : > "$OUTPUT_PATH"
453
+
454
+ while IFS= read -r line || [[ -n "$line" ]]; do
455
+ if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]]; then
456
+ local key="${BASH_REMATCH[1]}"
457
+ local template_val="${BASH_REMATCH[2]}"
458
+ local value=''
459
+ local rendered_value=''
460
+
461
+ # Map known keys to computed/discovered values, fallback to env/template.
462
+ case "$key" in
463
+ IS_CI)
464
+ value="$is_ci"
465
+ ;;
466
+ GIT_BRANCH)
467
+ value="$git_branch"
468
+ ;;
469
+ GIT_URL)
470
+ value="${MATCH_GIT_URL:-}"
471
+ ;;
472
+ FIREBASE_SERVICE_ACCOUNT)
473
+ value="$firebase_service_account_b64"
474
+ # Always quote this value explicitly to avoid dotenv parsing issues.
475
+ rendered_value="\"$value\""
476
+ ;;
477
+ FASTLANE_FIREBASE_APP_ID_ANDROID)
478
+ value="$android_app_id"
479
+ ;;
480
+ FASTLANE_FIREBASE_APP_ID_IOS)
481
+ value="$ios_app_id"
482
+ ;;
483
+ FASTLANE_FIREBASE_APP_ID)
484
+ value="$legacy_firebase_app_id"
485
+ ;;
486
+ FASTLANE_APP_IDENTIFIER)
487
+ value="${FASTLANE_APP_IDENTIFIER}"
488
+ ;;
489
+ FASTLANE_APP_IDENTIFIERS)
490
+ value="$app_identifiers"
491
+ ;;
492
+ KEY_ALIAS)
493
+ value="${KEY_ALIAS:-production}"
494
+ ;;
495
+ *)
496
+ # Prefer runtime env var, else preserve template default literal.
497
+ if [[ -n "${!key+x}" ]]; then
498
+ value="${!key}"
499
+ rendered_value="$(dotenv_escape "$value")"
500
+ else
501
+ rendered_value="$template_val"
502
+ fi
503
+ ;;
504
+ esac
505
+
506
+ if [[ -z "$rendered_value" ]]; then
507
+ rendered_value="$(dotenv_escape "$value")"
508
+ fi
509
+ printf '%s=%s\n' "$key" "$rendered_value" >> "$OUTPUT_PATH"
510
+ else
511
+ printf '%s\n' "$line" >> "$OUTPUT_PATH"
512
+ fi
513
+ done < "$TEMPLATE_PATH"
514
+
515
+ chmod 600 "$OUTPUT_PATH"
516
+ log "Wrote $OUTPUT_PATH"
517
+ }
518
+
519
+ main() {
520
+ while [[ $# -gt 0 ]]; do
521
+ case "$1" in
522
+ --env-file)
523
+ ENV_FILE_PATH="$2"
524
+ shift 2
525
+ ;;
526
+ --template)
527
+ TEMPLATE_PATH="$2"
528
+ shift 2
529
+ ;;
530
+ --output)
531
+ OUTPUT_PATH="$2"
532
+ OUTPUT_PATH_EXPLICIT=1
533
+ shift 2
534
+ ;;
535
+ --service-account-key)
536
+ SERVICE_ACCOUNT_KEY_PATH="$2"
537
+ shift 2
538
+ ;;
539
+ --service-account-name)
540
+ SERVICE_ACCOUNT_NAME="$2"
541
+ shift 2
542
+ ;;
543
+ --appdist-role)
544
+ FIREBASE_APPDIST_ROLE="$2"
545
+ shift 2
546
+ ;;
547
+ --yes)
548
+ AUTO_APPROVE=1
549
+ shift
550
+ ;;
551
+ -h|--help)
552
+ usage
553
+ exit 0
554
+ ;;
555
+ *)
556
+ die "Unknown argument: $1"
557
+ ;;
558
+ esac
559
+ done
560
+
561
+ load_env_file_if_configured
562
+ preflight_checks
563
+
564
+ if [[ ! "$ENVIRONMENT" =~ ^(development|staging|production)$ ]]; then
565
+ die "ENVIRONMENT must be one of: development, staging, production. Got: $ENVIRONMENT"
566
+ fi
567
+ if [[ "$OUTPUT_PATH_EXPLICIT" -eq 0 ]]; then
568
+ OUTPUT_PATH="fastlane/.env.${ENVIRONMENT}"
569
+ fi
570
+
571
+ local gcloud_account
572
+ gcloud_account="$(get_active_gcloud_account || true)"
573
+ local firebase_account
574
+ firebase_account="$(get_active_firebase_account || true)"
575
+ confirm_accounts "$gcloud_account" "$firebase_account"
576
+
577
+ # Use explicit Android app id when provided; otherwise reuse iOS bundle id.
578
+ local android_application_id="${ANDROID_APPLICATION_ID:-$FASTLANE_APP_IDENTIFIER}"
579
+ local firebase_app_name_android="${FIREBASE_APP_NAME_ANDROID:-${android_application_id}-android}"
580
+ local firebase_app_name_ios="${FIREBASE_APP_NAME_IOS:-${FASTLANE_APP_IDENTIFIER}-ios}"
581
+
582
+ local android_app_id
583
+ android_app_id="$(ensure_firebase_app "ANDROID" "$android_application_id" "$firebase_app_name_android")"
584
+
585
+ local android_devdebug_signing_hashes
586
+ android_devdebug_signing_hashes="$(get_android_devdebug_hashes)"
587
+ local android_devdebug_sha1
588
+ android_devdebug_sha1="$(sed -n '1p' <<<"$android_devdebug_signing_hashes")"
589
+ local android_devdebug_sha256
590
+ android_devdebug_sha256="$(sed -n '2p' <<<"$android_devdebug_signing_hashes")"
591
+ ensure_firebase_android_hash "$android_app_id" "$android_devdebug_sha1" "SHA1"
592
+ ensure_firebase_android_hash "$android_app_id" "$android_devdebug_sha256" "SHA-256"
593
+ download_android_google_services_json "$android_app_id" || true
594
+
595
+ local ios_app_id
596
+ ios_app_id="$(ensure_firebase_app "IOS" "$FASTLANE_APP_IDENTIFIER" "$firebase_app_name_ios")"
597
+ download_ios_google_service_info_plist "$ios_app_id" || true
598
+
599
+ local sa_email="${SERVICE_ACCOUNT_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com"
600
+ ensure_service_account "$sa_email" || true
601
+ ensure_service_account_key "$sa_email" "$SERVICE_ACCOUNT_KEY_PATH" || true
602
+
603
+ # Fastfile currently expects FIREBASE_SERVICE_ACCOUNT as base64-encoded JSON.
604
+ local firebase_sa_b64
605
+ firebase_sa_b64=''
606
+ if [[ -s "$SERVICE_ACCOUNT_KEY_PATH" ]]; then
607
+ firebase_sa_b64="$(get_base64_file "$SERVICE_ACCOUNT_KEY_PATH")"
608
+ fi
609
+
610
+ render_env_file "$android_app_id" "$ios_app_id" "$firebase_sa_b64"
611
+
612
+ log "Done. Firebase app IDs and service account material written to $OUTPUT_PATH"
613
+ }
614
+
615
+ main "$@"
@@ -1,54 +1,39 @@
1
1
  import { StateCreator, create } from 'zustand';
2
2
  import { PersistOptions, createJSONStorage, persist } from 'zustand/middleware';
3
3
 
4
- import { MmkvStorage } from '@/stores/mmkvStorage';
5
-
6
- const initialState = {
7
- _hasHydrated: false,
8
- user: null,
9
- };
4
+ import { createUserSlice, UserSlice } from './user.slice';
10
5
 
11
- // TODO: Replace this with actual user type from api-lib
12
- type User = {
13
- id: string;
14
- email: string;
15
- firstName: string;
16
- lastName: string;
17
- role: string;
18
- };
6
+ import { MmkvStorage } from '@/stores/mmkvStorage';
19
7
 
20
- export type LocalStorageState = {
8
+ export type LocalStorageStore = UserSlice & {
21
9
  _hasHydrated: boolean;
22
10
  setHasHydrated: (hasHydrated: boolean) => void;
23
- setUser: (user: User | null) => void;
24
- signOut: () => void;
25
- user: User | null;
11
+ clear: () => void;
26
12
  };
27
13
 
28
14
  type MyPersist = (
29
- config: StateCreator<LocalStorageState>,
30
- options: PersistOptions<LocalStorageState>,
31
- ) => StateCreator<LocalStorageState>;
15
+ config: StateCreator<LocalStorageStore>,
16
+ options: PersistOptions<LocalStorageStore>,
17
+ ) => StateCreator<LocalStorageStore>;
32
18
 
33
- export const useLocalStorageState = create<LocalStorageState, []>(
19
+ export const useLocalStorageStore = create<LocalStorageStore, []>(
34
20
  (persist as unknown as MyPersist)(
35
- (set, get) => ({
36
- _hasHydrated: initialState._hasHydrated,
37
- setHasHydrated: (hasHydrated) =>
38
- set({
39
- _hasHydrated: hasHydrated,
40
- }),
41
- setUser: (user) =>
42
- set({
43
- user,
44
- }),
45
- signOut: () => {
46
- set({
47
- user: initialState.user,
48
- });
49
- },
50
- user: initialState.user,
51
- }),
21
+ (set, get, store) =>
22
+ (() => {
23
+ const userSlice = createUserSlice(set, get, store);
24
+
25
+ return {
26
+ ...userSlice,
27
+ _hasHydrated: false,
28
+ clear: () => {
29
+ userSlice.signOut();
30
+ },
31
+ setHasHydrated: (hasHydrated) =>
32
+ set({
33
+ _hasHydrated: hasHydrated,
34
+ }),
35
+ };
36
+ })(),
52
37
  {
53
38
  name: 'local-storage',
54
39
  onRehydrateStorage: () => (state) => {
@@ -0,0 +1,23 @@
1
+ import { StateCreator } from 'zustand';
2
+
3
+ export type User = {
4
+ id: string;
5
+ email: string;
6
+ firstName: string;
7
+ lastName: string;
8
+ role: string;
9
+ };
10
+
11
+ export type UserSlice = {
12
+ setUser: (user: User | null) => void;
13
+ signOut: () => void;
14
+ user: User | null;
15
+ clear: () => void;
16
+ };
17
+
18
+ export const createUserSlice: StateCreator<UserSlice> = (set) => ({
19
+ clear: () => set({ user: null }),
20
+ setUser: (user: User | null) => set({ user }),
21
+ signOut: () => set({ user: null }),
22
+ user: null,
23
+ });