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 +1 -1
- package/package.json +1 -1
- package/templates/19.7.0/apps/mobile/.ignorefile +2 -0
- package/templates/19.7.0/apps/mobile/android/app/build.gradle +1 -1
- package/templates/21.2.2/apps/mobile/.ignorefile +2 -0
- package/templates/21.2.2/apps/mobile/android/app/build.gradle +1 -1
- package/templates/shared/.ignorefile +2 -0
- package/templates/shared/apps/mobile/README.generate_fastlane_env.md +118 -0
- package/templates/shared/apps/mobile/fastlane/Fastfile +2 -2
- package/templates/shared/apps/mobile/generate_fastlane_env.sh +615 -0
- package/templates/shared/apps/mobile/src/stores/local-storage.store.ts +24 -39
- package/templates/shared/apps/mobile/src/stores/user.slice.ts +23 -0
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.
|
|
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
|
@@ -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
|
-
|
|
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"]
|
|
@@ -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
|
-
|
|
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"]
|
|
@@ -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['
|
|
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['
|
|
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 {
|
|
5
|
-
|
|
6
|
-
const initialState = {
|
|
7
|
-
_hasHydrated: false,
|
|
8
|
-
user: null,
|
|
9
|
-
};
|
|
4
|
+
import { createUserSlice, UserSlice } from './user.slice';
|
|
10
5
|
|
|
11
|
-
|
|
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
|
|
8
|
+
export type LocalStorageStore = UserSlice & {
|
|
21
9
|
_hasHydrated: boolean;
|
|
22
10
|
setHasHydrated: (hasHydrated: boolean) => void;
|
|
23
|
-
|
|
24
|
-
signOut: () => void;
|
|
25
|
-
user: User | null;
|
|
11
|
+
clear: () => void;
|
|
26
12
|
};
|
|
27
13
|
|
|
28
14
|
type MyPersist = (
|
|
29
|
-
config: StateCreator<
|
|
30
|
-
options: PersistOptions<
|
|
31
|
-
) => StateCreator<
|
|
15
|
+
config: StateCreator<LocalStorageStore>,
|
|
16
|
+
options: PersistOptions<LocalStorageStore>,
|
|
17
|
+
) => StateCreator<LocalStorageStore>;
|
|
32
18
|
|
|
33
|
-
export const
|
|
19
|
+
export const useLocalStorageStore = create<LocalStorageStore, []>(
|
|
34
20
|
(persist as unknown as MyPersist)(
|
|
35
|
-
(set, get) =>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
});
|